diff --git a/packages/app/studio/src/service/SessionService.ts b/packages/app/studio/src/service/SessionService.ts index c8ebd9e84..756c11ec8 100644 --- a/packages/app/studio/src/service/SessionService.ts +++ b/packages/app/studio/src/service/SessionService.ts @@ -56,6 +56,9 @@ export class SessionService implements MutableObservableValue>>): Terminable { observer(this) @@ -65,6 +68,8 @@ export class SessionService implements MutableObservableValue { return this.#session.getValue() @@ -74,6 +79,8 @@ export class SessionService implements MutableObservableValue { return this.#session.getValue().ifSome(async session => { @@ -87,7 +94,11 @@ export class SessionService implements MutableObservableValue { const {status, value} = await Promises.tryCatch(ProjectDialogs.showBrowseDialog(this.#service)) if (status === "resolved") { @@ -101,6 +112,7 @@ export class SessionService implements MutableObservableValue { console.debug(`load '${name}'`) @@ -135,6 +148,8 @@ export class SessionService implements MutableObservableValue { return this.#session.getValue().ifSome(async session => { @@ -164,6 +179,8 @@ export class SessionService implements MutableObservableValue { try { @@ -179,6 +196,8 @@ export class SessionService implements MutableObservableValue { this.#session.getValue().ifSome(async session => { @@ -198,6 +217,8 @@ export class SessionService implements MutableObservableValue { try { diff --git a/packages/app/studio/src/service/StudioService.ts b/packages/app/studio/src/service/StudioService.ts index 6cec7e91d..dc4f93545 100644 --- a/packages/app/studio/src/service/StudioService.ts +++ b/packages/app/studio/src/service/StudioService.ts @@ -429,8 +429,15 @@ export class StudioService implements ProjectEnv { } } + /** Start a new session from an existing project. */ fromProject(project: Project, name: string): void {this.sessionService.fromProject(project, name)} + /** + * Execute a procedure only if a project session is available. + * + * @param procedure - Callback invoked with the active project. + * @returns Option of the callback result. + */ runIfProject(procedure: Func): Option { return this.sessionService.getValue().map(({project}) => procedure(project)) } @@ -448,17 +455,33 @@ export class StudioService implements ProjectEnv { }) } + /** + * Switch the workspace to another screen. + * + * @param key - Identifier of the screen or `null` for the default. + */ switchScreen(key: Nullable): void { this.layout.screen.setValue(key) RouteLocation.get().navigateTo("/") } + /** + * Register a factory used to create a footer label component. + * + * @param factory - Provider creating footer labels on demand. + */ registerFooter(factory: Provider): void { this.#factoryFooterLabel = Option.wrap(factory) } + /** + * Accessor for the registered footer label factory. + */ factoryFooterLabel(): Option> {return this.#factoryFooterLabel} + /** + * Notify listeners to reset waveform peaks across the UI. + */ resetPeaks(): void {this.#signals.notify({type: "reset-peaks"})} #startAudioWorklet(terminator: Terminator, project: Project): void { diff --git a/packages/app/studio/src/service/StudioSignal.ts b/packages/app/studio/src/service/StudioSignal.ts index 25664512b..87e45e230 100644 --- a/packages/app/studio/src/service/StudioSignal.ts +++ b/packages/app/studio/src/service/StudioSignal.ts @@ -6,6 +6,10 @@ import {Sample} from "@opendaw/studio-adapters" * These complement the {@link SessionService} lifecycle and other session * oriented services. * + * @remarks + * The set is intentionally small but can grow as new UI elements are + * introduced. + * * ```mermaid * classDiagram * class StudioSignal @@ -24,4 +28,4 @@ export type StudioSignal = } | { /** Delete a project identified by its metadata. */ type: "delete-project", meta: ProjectMeta -} \ No newline at end of file +} diff --git a/packages/app/studio/src/service/SyncLogService.ts b/packages/app/studio/src/service/SyncLogService.ts index 88d760da0..d83f01fa3 100644 --- a/packages/app/studio/src/service/SyncLogService.ts +++ b/packages/app/studio/src/service/SyncLogService.ts @@ -21,6 +21,7 @@ export namespace SyncLogService { * Start a new SyncLog and attach it to the current project. * * @param service - Studio service providing the project context. + * @returns Promise resolving when the SyncLog is ready. */ export const start = async (service: StudioService) => { if (!isDefined(window.showSaveFilePicker)) {return} @@ -43,6 +44,7 @@ export namespace SyncLogService { * Append commits to an existing SyncLog file selected by the user. * * @param service - Studio service providing the project context. + * @returns Promise resolving when all commits have been appended. */ export const append = async (service: StudioService) => { const openResult = await Promises.tryCatch(window.showOpenFilePicker(FilePickerAcceptTypes.ProjectSyncLog)) diff --git a/packages/app/studio/src/ui/components/ControlIndicator.tsx b/packages/app/studio/src/ui/components/ControlIndicator.tsx index e4d57d79e..672a826b1 100644 --- a/packages/app/studio/src/ui/components/ControlIndicator.tsx +++ b/packages/app/studio/src/ui/components/ControlIndicator.tsx @@ -10,7 +10,12 @@ export interface ControlIndicatorProps { parameter: AutomatableParameterFieldAdapter } -/** Adds an automation indicator to its children. */ +/** + * Adds an automation indicator to its children. + * + * @param children - Elements to decorate with the indicator. + * @returns Wrapped element reflecting automation state. + */ export const ControlIndicator = ({lifecycle, parameter}: ControlIndicatorProps, children: JsxValue) => { const element: HTMLElement = {children} lifecycle.own(parameter.catchupAndSubscribeControlSources({ diff --git a/packages/app/studio/src/ui/components/NumberInput.tsx b/packages/app/studio/src/ui/components/NumberInput.tsx index 8ac7f376a..81f2feaaf 100644 --- a/packages/app/studio/src/ui/components/NumberInput.tsx +++ b/packages/app/studio/src/ui/components/NumberInput.tsx @@ -23,7 +23,11 @@ export interface NumberInputProps { step?: number } -/** Editable numeric field with keyboard controls. */ +/** + * Editable numeric field with keyboard controls. + * + * @returns Element representing the numeric input. + */ export const NumberInput = ({lifecycle, model, negativeWarning, className, maxChars, mapper, step}: NumberInputProps) => { step ??= 1.0 maxChars ??= 3 diff --git a/packages/docs/docs-dev/architecture/state.md b/packages/docs/docs-dev/architecture/state.md new file mode 100644 index 000000000..f208d95b0 --- /dev/null +++ b/packages/docs/docs-dev/architecture/state.md @@ -0,0 +1,15 @@ +# State + +The Studio maintains its user interface and engine state through a collection +of observable values and service classes. Core helpers from the +`@opendaw/lib-std` package provide `Observable` and `MutableObservableValue` +primitives that allow components to react to changes without tight coupling. + +High level services such as `SessionService` and `StudioService` expose these +observables to coordinate project loading, transport control and other +application features. Components subscribe to these values and update the UI or +audio engine in response. + +For commit history and synchronisation details see the +[SyncLog architecture](sync-log.md). + diff --git a/packages/docs/docs-user/ui-tour.md b/packages/docs/docs-user/ui-tour.md index 65dd53286..85a684b05 100644 --- a/packages/docs/docs-user/ui-tour.md +++ b/packages/docs/docs-user/ui-tour.md @@ -51,6 +51,7 @@ For internal tooling and diagnostics see: - [Components](../docs-dev/ui/pages/components.md) - [Advanced Components](../docs-dev/ui/components/advanced.md) - [Diagnostics](../docs-dev/ui/pages/diagnostics.md) +- [State Architecture](../docs-dev/architecture/state.md) - [Icons](../docs-dev/ui/pages/icons.md) – [icon docs](../docs-dev/ui/icons/overview.md) - [Manuals](../docs-dev/ui/pages/manuals.md) - [Navigation](../docs-dev/ui/navigation/overview.md) diff --git a/packages/lib/std/src/listeners.ts b/packages/lib/std/src/listeners.ts index 97507d6ef..cb96b481e 100644 --- a/packages/lib/std/src/listeners.ts +++ b/packages/lib/std/src/listeners.ts @@ -4,37 +4,65 @@ import { Subscription, Terminable } from "./terminable"; import { int, Procedure, safeExecute } from "./lang"; +/** + * Collection managing lifecycle of event listeners. + * + * @template T Type describing the listener interface. + */ export class Listeners implements Terminable { readonly #set = new Set(); readonly #proxy: Required; constructor() { - this.#proxy = new Proxy({}, { - get: - (_: never, func: string): (() => void) => - (...args: unknown[]): void => - this.#set.forEach((listener: any) => { - if (Object.getPrototypeOf(listener) === Object.getPrototypeOf({})) { - return safeExecute(listener[func], ...args); - } - return listener[func]?.apply(listener, args); - }), - } as const) as Required; + this.#proxy = new Proxy( + {}, + { + get: + (_: never, func: string): (() => void) => + (...args: unknown[]): void => + this.#set.forEach((listener: any) => { + if (Object.getPrototypeOf(listener) === Object.getPrototypeOf({})) { + return safeExecute(listener[func], ...args); + } + return listener[func]?.apply(listener, args); + }), + } as const, + ) as Required; } + /** + * Proxy forwarding method calls to all registered listeners. + */ get proxy(): Required { return this.#proxy; } + + /** Number of registered listeners. */ get size(): int { return this.#set.size; } + + /** + * Register a listener instance. + * + * @param listener - Listener to add. + * @returns Subscription used to remove the listener. + */ subscribe(listener: T): Subscription { this.#set.add(listener); return { terminate: () => this.#set.delete(listener) }; } + + /** + * Iterate through all listeners with a callback. + * + * @param procedure - Callback executed for each listener. + */ forEach(procedure: Procedure): void { this.#set.forEach(procedure); } + + /** Remove all registered listeners. */ terminate(): void { this.#set.clear(); } diff --git a/packages/lib/std/src/observables.ts b/packages/lib/std/src/observables.ts index 92b56dabb..a62818f11 100644 --- a/packages/lib/std/src/observables.ts +++ b/packages/lib/std/src/observables.ts @@ -1,3 +1,6 @@ +/** + * Observable primitives and helpers. + */ import {Subscription, Terminable} from "./terminable" import {Notifier} from "./notifier" import {Option} from "./option" @@ -20,6 +23,12 @@ export interface ObservableValue extends Observable> { } export namespace ObservableValue { + /** + * Creates an immutable observable that always exposes the given value. + * + * @param value - The value to wrap. + * @returns Observable that never changes. + */ export const make = (value: T): ObservableValue => new class implements ObservableValue { getValue(): T {return value} subscribe(_observer: Observer>): Subscription {return Terminable.Empty} @@ -52,6 +61,9 @@ export namespace MutableObservableValue { /** * Creates a view on the given boolean observable that presents and * writes the negated value. + * + * @param observableValue - Source observable boolean. + * @returns Observable reflecting the inverted state. */ export const inverseBoolean = (observableValue: MutableObservableValue): MutableObservableValue => new class implements MutableObservableValue { diff --git a/packages/lib/std/src/observers.ts b/packages/lib/std/src/observers.ts index 2c8addb62..1fb40e607 100644 --- a/packages/lib/std/src/observers.ts +++ b/packages/lib/std/src/observers.ts @@ -1,4 +1,8 @@ +/** + * Observation callback utilities. + * @packageDocumentation + */ import {Procedure} from "./lang" /** Callback invoked when an {@link Observable} emits a value. */ -export type Observer = Procedure \ No newline at end of file +export type Observer = Procedure