add auto_focus parameter (and refactor TS)

This commit is contained in:
Aran-Fey
2025-03-09 20:07:51 +01:00
parent 312abe62cc
commit dea9cbbf71
75 changed files with 673 additions and 653 deletions

View File

@@ -63,6 +63,7 @@ import { ThemeContextSwitcherComponent } from "./components/themeContextSwitcher
import { TooltipComponent } from "./components/tooltip";
import { WebviewComponent } from "./components/webview";
import { GraphEditorComponent } from "./components/graphEditor/graphEditor";
import { KeyboardFocusableComponent } from "./components/keyboardFocusableComponent";
const COMPONENT_CLASSES = {
"Button-builtin": ButtonComponent,
@@ -334,10 +335,7 @@ export function updateComponentStates(
component.updateElement(deltaState, latentComponents);
// Update the component's state
component.state = {
...component.state,
...deltaState,
};
Object.assign(component.state, deltaState);
}
// Notify the parents of all elements whose `_grow_` changed to update their
@@ -397,11 +395,6 @@ export function recursivelyDeleteComponent(component: ComponentBase): void {
component.element.remove();
}
function canHaveKeyboardFocus(instance: ComponentBase): boolean {
// @ts-expect-error
return typeof instance.grabKeyboardFocus === "function";
}
function restoreKeyboardFocus(
focusedComponent: ComponentBase,
latentComponents: Set<ComponentBase>
@@ -415,7 +408,7 @@ function restoreKeyboardFocus(
// itself might be about to die.
let rootComponent = getRootComponent();
let current = focusedComponent;
let winner: ComponentBase | null = null;
let winner: KeyboardFocusableComponent | null = null;
while (current !== rootComponent) {
// If this component is dead, no child of it can get the keyboard focus
@@ -425,7 +418,10 @@ function restoreKeyboardFocus(
// If we don't currently know of a focusable (and live) component, check
// if this one fits the bill
else if (winner === null && canHaveKeyboardFocus(current)) {
else if (
winner === null &&
current instanceof KeyboardFocusableComponent
) {
winner = current;
}
@@ -434,7 +430,6 @@ function restoreKeyboardFocus(
// We made it to the root. Do we have a winner?
if (winner !== null) {
// @ts-expect-error
winner.grabKeyboardFocus();
}
}

View File

@@ -1,21 +1,19 @@
import { applySwitcheroo } from "../designApplication";
import { ColorSet, ComponentId } from "../dataModels";
import { ComponentBase, ComponentState } from "./componentBase";
import { ComponentBase, ComponentState, DeltaState } from "./componentBase";
import { RippleEffect } from "../rippleEffect";
import { markEventAsHandled } from "../eventHandling";
import { getAllocatedHeightInPx, getAllocatedWidthInPx } from "../utils";
type AbstractButtonState = ComponentState & {
shape?: "pill" | "rounded" | "rectangle" | "circle";
style?: "major" | "minor" | "colored-text" | "plain-text";
color?: ColorSet;
content?: ComponentId;
is_sensitive?: boolean;
shape: "pill" | "rounded" | "rectangle" | "circle";
style: "major" | "minor" | "colored-text" | "plain-text";
color: ColorSet;
content: ComponentId;
is_sensitive: boolean;
};
abstract class AbstractButtonComponent extends ComponentBase {
declare state: Required<AbstractButtonState>;
abstract class AbstractButtonComponent extends ComponentBase<AbstractButtonState> {
// This is the element with the `rio-button` class. The subclass is
// responsible for creating it (by calling `createButtonElement()`).
protected buttonElement: HTMLElement;
@@ -66,7 +64,7 @@ abstract class AbstractButtonComponent extends ComponentBase {
}
updateElement(
deltaState: AbstractButtonState,
deltaState: DeltaState<AbstractButtonState>,
latentComponents: Set<ComponentBase>
): void {
super.updateElement(deltaState, latentComponents);
@@ -133,8 +131,6 @@ export type ButtonState = AbstractButtonState & {
};
export class ButtonComponent extends AbstractButtonComponent {
declare state: Required<ButtonState>;
createElement(): HTMLElement {
this.buttonElement = this.createButtonElement();
this.buttonElement.role = "button";
@@ -148,8 +144,6 @@ export type IconButtonState = AbstractButtonState & {
};
export class IconButtonComponent extends AbstractButtonComponent {
declare state: Required<IconButtonState>;
private resizeObserver: ResizeObserver;
protected createElement(): HTMLElement {

View File

@@ -1,4 +1,4 @@
import { ComponentBase, ComponentState } from "./componentBase";
import { ComponentBase, ComponentState, DeltaState } from "./componentBase";
import { applyIcon } from "../designApplication";
import { markEventAsHandled } from "../eventHandling";
@@ -7,17 +7,15 @@ const CALENDAR_HEIGHT = 17.8;
export type CalendarState = ComponentState & {
_type_: "Calendar-builtin";
selectedYear?: number;
selectedMonth?: number; // [1, 12]
selectedDay?: number; // [1, ...]
monthNamesLong?: Array<string>;
dayNamesLong?: Array<string>;
firstDayOfWeek?: number;
selectedYear: number;
selectedMonth: number; // [1, 12]
selectedDay: number; // [1, ...]
monthNamesLong: Array<string>;
dayNamesLong: Array<string>;
firstDayOfWeek: number;
};
export class CalendarComponent extends ComponentBase {
declare state: Required<CalendarState>;
export class CalendarComponent extends ComponentBase<CalendarState> {
// Internal HTML Elements
private prevYearButton: HTMLElement;
private prevMonthButton: HTMLElement;
@@ -104,7 +102,7 @@ export class CalendarComponent extends ComponentBase {
}
updateElement(
deltaState: CalendarState,
deltaState: DeltaState<CalendarState>,
latentComponents: Set<ComponentBase>
): void {
super.updateElement(deltaState, latentComponents);

View File

@@ -1,23 +1,21 @@
import { applySwitcheroo } from "../designApplication";
import { ColorSet, ComponentId } from "../dataModels";
import { RippleEffect } from "../rippleEffect";
import { ComponentBase, ComponentState } from "./componentBase";
import { ComponentBase, ComponentState, DeltaState } from "./componentBase";
import { markEventAsHandled } from "../eventHandling";
export type CardState = ComponentState & {
_type_: "Card-builtin";
content?: ComponentId;
corner_radius?: number | [number, number, number, number];
reportPress?: boolean;
ripple?: boolean;
elevate_on_hover?: boolean;
colorize_on_hover?: boolean;
color?: ColorSet;
content: ComponentId;
corner_radius: number | [number, number, number, number];
reportPress: boolean;
ripple: boolean;
elevate_on_hover: boolean;
colorize_on_hover: boolean;
color: ColorSet;
};
export class CardComponent extends ComponentBase {
declare state: Required<CardState>;
export class CardComponent extends ComponentBase<CardState> {
// If this card has a ripple effect, this is the ripple instance. `null`
// otherwise.
private rippleInstance: RippleEffect | null = null;
@@ -46,7 +44,7 @@ export class CardComponent extends ComponentBase {
}
updateElement(
deltaState: CardState,
deltaState: DeltaState<CardState>,
latentComponents: Set<ComponentBase>
): void {
super.updateElement(deltaState, latentComponents);

View File

@@ -1,15 +1,13 @@
import { applyIcon } from "../designApplication";
import { ComponentBase, ComponentState } from "./componentBase";
import { ComponentBase, ComponentState, DeltaState } from "./componentBase";
export type CheckboxState = ComponentState & {
_type_: "Checkbox-builtin";
is_on?: boolean;
is_sensitive?: boolean;
is_on: boolean;
is_sensitive: boolean;
};
export class CheckboxComponent extends ComponentBase {
declare state: Required<CheckboxState>;
export class CheckboxComponent extends ComponentBase<CheckboxState> {
private checkboxElement: HTMLInputElement;
private borderElement: HTMLElement;
private checkElement: HTMLElement;
@@ -52,7 +50,7 @@ export class CheckboxComponent extends ComponentBase {
}
updateElement(
deltaState: CheckboxState,
deltaState: DeltaState<CheckboxState>,
latentComponents: Set<ComponentBase>
): void {
super.updateElement(deltaState, latentComponents);

View File

@@ -1,21 +1,19 @@
import { ComponentBase, ComponentState } from "./componentBase";
import { ComponentBase, ComponentState, DeltaState } from "./componentBase";
import { ComponentId } from "../dataModels";
export type ClassContainerState = ComponentState & {
_type_: "ClassContainer-builtin";
content?: ComponentId | null;
classes?: string[];
content: ComponentId | null;
classes: string[];
};
export class ClassContainerComponent extends ComponentBase {
declare state: Required<ClassContainerState>;
export class ClassContainerComponent extends ComponentBase<ClassContainerState> {
createElement(): HTMLElement {
return document.createElement("div");
}
updateElement(
deltaState: ClassContainerState,
deltaState: DeltaState<ClassContainerState>,
latentComponents: Set<ComponentBase>
): void {
super.updateElement(deltaState, latentComponents);

View File

@@ -1,4 +1,4 @@
import { ComponentBase, ComponentState } from "./componentBase";
import { ComponentBase, ComponentState, DeltaState } from "./componentBase";
// This import decides which languages are supported by `highlight.js`. See
// their docs for details:
@@ -11,15 +11,6 @@ import { setClipboard } from "../utils";
import { applyIcon } from "../designApplication";
import { markEventAsHandled } from "../eventHandling";
export type CodeBlockState = ComponentState & {
_type_: "CodeBlock-builtin";
code?: string;
language?: string | null;
show_controls?: boolean;
scroll_code_x?: "never" | "auto" | "always";
scroll_code_y?: "never" | "auto" | "always";
};
/// Contains additional aliases for languages that are not recognized by
/// highlight.js
const languageAliases: { [key: string]: string } = {
@@ -133,16 +124,23 @@ export function convertDivToCodeBlock(
}
}
export class CodeBlockComponent extends ComponentBase {
declare state: Required<CodeBlockState>;
export type CodeBlockState = ComponentState & {
_type_: "CodeBlock-builtin";
code: string;
language: string | null;
show_controls: boolean;
scroll_code_x: "never" | "auto" | "always";
scroll_code_y: "never" | "auto" | "always";
};
export class CodeBlockComponent extends ComponentBase<CodeBlockState> {
createElement(): HTMLElement {
const element = document.createElement("div");
return element;
}
updateElement(
deltaState: CodeBlockState,
deltaState: DeltaState<CodeBlockState>,
latentComponents: Set<ComponentBase>
): void {
super.updateElement(deltaState, latentComponents);

View File

@@ -1,20 +1,18 @@
import hljs from "highlight.js/lib/common";
import { componentsByElement, componentsById } from "../componentManagement";
import { ComponentId } from "../dataModels";
import { ComponentBase, ComponentState } from "./componentBase";
import { ComponentBase, ComponentState, DeltaState } from "./componentBase";
import { applyIcon } from "../designApplication";
export type CodeExplorerState = ComponentState & {
_type_: "CodeExplorer-builtin";
source_code?: string;
build_result?: ComponentId;
line_indices_to_component_keys?: (string | number | null)[];
style?: "horizontal" | "vertical";
source_code: string;
build_result: ComponentId;
line_indices_to_component_keys: (string | number | null)[];
style: "horizontal" | "vertical";
};
export class CodeExplorerComponent extends ComponentBase {
declare state: Required<CodeExplorerState>;
export class CodeExplorerComponent extends ComponentBase<CodeExplorerState> {
private sourceCodeElement: HTMLElement;
private arrowElement: HTMLElement;
private buildResultElement: HTMLElement;
@@ -62,7 +60,7 @@ export class CodeExplorerComponent extends ComponentBase {
}
updateElement(
deltaState: CodeExplorerState,
deltaState: DeltaState<CodeExplorerState>,
latentComponents: Set<ComponentBase>
): void {
super.updateElement(deltaState, latentComponents);

View File

@@ -1,17 +1,15 @@
import { Color } from "../dataModels";
import { ComponentBase, ComponentState } from "./componentBase";
import { ComponentBase, ComponentState, DeltaState } from "./componentBase";
import { hsvToRgb, rgbToHsv, rgbToHex, rgbaToHex } from "../colorConversion";
import { markEventAsHandled } from "../eventHandling";
export type ColorPickerState = ComponentState & {
_type_: "ColorPicker-builtin";
color?: Color;
pick_opacity?: boolean;
color: Color;
pick_opacity: boolean;
};
export class ColorPickerComponent extends ComponentBase {
declare state: Required<ColorPickerState>;
export class ColorPickerComponent extends ComponentBase<ColorPickerState> {
private colorSquare: HTMLElement;
private squareKnob: HTMLElement;
@@ -116,7 +114,7 @@ export class ColorPickerComponent extends ComponentBase {
}
updateElement(
deltaState: ColorPickerState,
deltaState: DeltaState<ColorPickerState>,
latentComponents: Set<ComponentBase>
): void {
super.updateElement(deltaState, latentComponents);
@@ -357,7 +355,7 @@ export class ColorPickerComponent extends ComponentBase {
setFromUserHex(event: Event) {
// Try to parse the value
let color = this.lenientlyParseColorHex(event.target.value);
let color = this.lenientlyParseColorHex(this.selectedColorLabel.value);
// Invalid color
if (color === null) {
@@ -375,7 +373,7 @@ export class ColorPickerComponent extends ComponentBase {
this.matchComponentToSelectedHsv();
// Deselect the text input
event.target.blur();
this.selectedColorLabel.blur();
// Send the final color to the frontend
this.sendMessageToBackend({

View File

@@ -20,39 +20,38 @@ import { devToolsConnector } from "../app";
export type ComponentState = {
// The component type's unique id. Crucial so the client knows what kind of
// component to spawn.
_type_?: string;
readonly _type_: string;
// Debugging information. Useful both for developing rio itself, and also
// displayed to developers in Rio's dev tools
_python_type_?: string;
_python_type_: string;
// Debugging information
_key_?: string | number | null;
_key_: string | number | null;
// How much space to leave on the left, top, right, bottom
_margin_?: [number, number, number, number];
_margin_: [number, number, number, number];
// Explicit size request, if any
_min_size_?: [number, number];
_min_size_: [number, number];
// Maximum size, if any
// MAX-SIZE-BRANCH _max_size_?: [number | null, number | null];
// Alignment of the component within its parent, if any
_align_?: [number | null, number | null];
_align_: [number | null, number | null];
// Scrolling behavior
// SCROLLING-REWORK _scroll_?: [RioScrollBehavior, RioScrollBehavior];
// Whether the component would like to receive additional space if there is
// any left over
_grow_?: [boolean, boolean];
_grow_: [boolean, boolean];
// Debugging information: The dev tools may not display components to the
// developer if they're considered internal
_rio_internal_?: boolean;
_rio_internal_: boolean;
};
/// Base class for all components
///
/// Note: Components that can have the keyboard focus must also implement a
/// `grabKeyboardFocus(): void` method.
export abstract class ComponentBase {
id: ComponentId;
element: HTMLElement;
export type DeltaState<S extends ComponentState> = Omit<Partial<S>, "_type_">;
state: Required<ComponentState>;
/// Base class for all components
export abstract class ComponentBase<S extends ComponentState = ComponentState> {
readonly id: ComponentId;
readonly element: HTMLElement;
readonly state: S;
// Reference to the parent component. If the component is about to be
// removed from the component tree (i.e. it's in `latent-components`), this
@@ -72,7 +71,7 @@ export abstract class ComponentBase {
private centerScrollElement: HTMLElement | null = null;
private innerScrollElement: HTMLElement | null = null;
constructor(id: ComponentId, state: Required<ComponentState>) {
constructor(id: ComponentId, state: S) {
this.id = id;
this.state = state;
@@ -110,7 +109,7 @@ export abstract class ComponentBase {
/// The `element` parameter is identical to `this.element`. It's passed as
/// an argument because it's more efficient than calling `this.element`.
updateElement(
deltaState: ComponentState,
deltaState: DeltaState<S>,
latentComponents: Set<ComponentBase>
): void {
if (deltaState._min_size_ !== undefined) {
@@ -551,15 +550,12 @@ export abstract class ComponentBase {
});
}
_setStateDontNotifyBackend(deltaState: object): void {
_setStateDontNotifyBackend(deltaState: DeltaState<S>): void {
// Trigger an update
this.updateElement(deltaState, null as any as Set<ComponentBase>);
// Set the state
this.state = {
...this.state,
...deltaState,
};
Object.assign(this.state, deltaState);
// Notify the dev tools, if any
if (devToolsConnector !== null) {
@@ -569,7 +565,7 @@ export abstract class ComponentBase {
}
}
setStateAndNotifyBackend(deltaState: object): void {
setStateAndNotifyBackend(deltaState: DeltaState<S>): void {
// Set the state. This also updates the component
this._setStateDontNotifyBackend(deltaState);

View File

@@ -1,12 +1,12 @@
import { devToolsConnector } from "../app";
import { applyIcon } from "../designApplication";
import { ComponentBase, ComponentState } from "./componentBase";
import { ComponentBase, ComponentState, DeltaState } from "./componentBase";
export type ComponentPickerState = ComponentState & {
_type_: "ComponentPicker-builtin";
};
export class ComponentPickerComponent extends ComponentBase {
export class ComponentPickerComponent extends ComponentBase<ComponentPickerState> {
protected createElement(): HTMLElement {
let element = document.createElement("div");
element.classList.add("rio-component-picker");

View File

@@ -1,6 +1,6 @@
import { componentsById } from "../componentManagement";
import { applyIcon } from "../designApplication";
import { ComponentBase, ComponentState } from "./componentBase";
import { ComponentBase, ComponentState, DeltaState } from "./componentBase";
import { Highlighter } from "../highlighter";
import {
getDisplayableChildren,
@@ -12,12 +12,10 @@ import { findComponentUnderMouse } from "../utils";
export type ComponentTreeState = ComponentState & {
_type_: "ComponentTree-builtin";
component_id?: number;
component_id: number;
};
export class ComponentTreeComponent extends ComponentBase {
declare state: Required<ComponentTreeState>;
export class ComponentTreeComponent extends ComponentBase<ComponentTreeState> {
private highlighter = new Highlighter();
private nodesByComponent: WeakMap<ComponentBase, HTMLElement> =
@@ -54,7 +52,7 @@ export class ComponentTreeComponent extends ComponentBase {
}
updateElement(
deltaState: ComponentTreeState,
deltaState: DeltaState<ComponentTreeState>,
latentComponents: Set<ComponentBase>
): void {
super.updateElement(deltaState, latentComponents);
@@ -344,7 +342,7 @@ export class ComponentTreeComponent extends ComponentBase {
private nodeNeedsRebuild(
component: ComponentBase,
deltaState: ComponentState
deltaState: DeltaState<ComponentState>
): boolean {
if ("key" in deltaState) {
return true;

View File

@@ -1,16 +1,14 @@
import { RippleEffect } from "../rippleEffect";
import { ComponentBase, ComponentState } from "./componentBase";
import { ComponentBase, ComponentState, DeltaState } from "./componentBase";
import { ComponentId } from "../dataModels";
export type CustomListItemState = ComponentState & {
_type_: "CustomListItem-builtin";
content?: ComponentId;
pressable?: boolean;
content: ComponentId;
pressable: boolean;
};
export class CustomListItemComponent extends ComponentBase {
declare state: Required<CustomListItemState>;
export class CustomListItemComponent extends ComponentBase<CustomListItemState> {
// If this item has a ripple effect, this is the ripple instance. `null`
// otherwise.
private rippleInstance: RippleEffect | null = null;
@@ -22,7 +20,7 @@ export class CustomListItemComponent extends ComponentBase {
}
updateElement(
deltaState: CustomListItemState,
deltaState: DeltaState<CustomListItemState>,
latentComponents: Set<ComponentBase>
): void {
super.updateElement(deltaState, latentComponents);

View File

@@ -1,14 +1,12 @@
import { setDevToolsConnector } from "../app";
import { ComponentBase, ComponentState } from "./componentBase";
import { ComponentBase, ComponentState, DeltaState } from "./componentBase";
import { ComponentTreeComponent } from "./componentTree";
export type DevToolsConnectorState = ComponentState & {
_type_: "DevToolsConnector-builtin";
};
export class DevToolsConnectorComponent extends ComponentBase {
declare state: Required<DevToolsConnectorState>;
export class DevToolsConnectorComponent extends ComponentBase<DevToolsConnectorState> {
// If component tree components exists, they register here
public componentTreeComponent: ComponentTreeComponent | null = null;

View File

@@ -5,19 +5,17 @@ import {
import { ComponentId } from "../dataModels";
import { FullscreenPositioner, PopupManager } from "../popupManager";
import { callRemoteMethodDiscardResponse } from "../rpc";
import { ComponentBase, ComponentState } from "./componentBase";
import { ComponentBase, ComponentState, DeltaState } from "./componentBase";
export type DialogContainerState = ComponentState & {
_type_: "DialogContainer-builtin";
content?: ComponentId;
owning_component_id?: ComponentId;
is_modal?: boolean;
is_user_closable?: boolean;
content: ComponentId;
owning_component_id: ComponentId;
is_modal: boolean;
is_user_closable: boolean;
};
export class DialogContainerComponent extends ComponentBase {
declare state: Required<DialogContainerState>;
export class DialogContainerComponent extends ComponentBase<DialogContainerState> {
private contentContainer: HTMLElement;
// Dialogs are displayed via a popup manager. While this isn't strictly
@@ -104,7 +102,7 @@ export class DialogContainerComponent extends ComponentBase {
}
updateElement(
deltaState: DialogContainerState,
deltaState: DeltaState<DialogContainerState>,
latentComponents: Set<ComponentBase>
): void {
super.updateElement(deltaState, latentComponents);

View File

@@ -1,24 +1,22 @@
import { pixelsPerRem } from "../app";
import { commitCss } from "../utils";
import { ComponentBase, ComponentState } from "./componentBase";
import { ComponentBase, ComponentState, DeltaState } from "./componentBase";
import { ColorSet, ComponentId } from "../dataModels";
import { applySwitcheroo } from "../designApplication";
import { markEventAsHandled } from "../eventHandling";
export type DrawerState = ComponentState & {
_type_: "Drawer-builtin";
anchor?: ComponentId;
content?: ComponentId;
side?: "left" | "right" | "top" | "bottom";
is_modal?: boolean;
is_open?: boolean;
is_user_openable?: boolean;
color?: ColorSet;
anchor: ComponentId;
content: ComponentId;
side: "left" | "right" | "top" | "bottom";
is_modal: boolean;
is_open: boolean;
is_user_openable: boolean;
color: ColorSet;
};
export class DrawerComponent extends ComponentBase {
declare state: Required<DrawerState>;
export class DrawerComponent extends ComponentBase<DrawerState> {
private anchorContainer: HTMLElement;
private contentOuterContainer: HTMLElement;
private contentInnerContainer: HTMLElement;
@@ -74,7 +72,7 @@ export class DrawerComponent extends ComponentBase {
}
updateElement(
deltaState: DrawerState,
deltaState: DeltaState<DrawerState>,
latentComponents: Set<ComponentBase>
): void {
super.updateElement(deltaState, latentComponents);

View File

@@ -1,27 +1,29 @@
import { ComponentBase, ComponentState } from "./componentBase";
import { ComponentBase, DeltaState } from "./componentBase";
import { applyIcon } from "../designApplication";
import { InputBox, InputBoxStyle } from "../inputBox";
import { markEventAsHandled } from "../eventHandling";
import { DropdownPositioner, PopupManager } from "../popupManager";
export type DropdownState = ComponentState & {
_type_: "Dropdown-builtin";
optionNames?: string[];
label?: string;
accessibility_label?: string;
style?: InputBoxStyle;
selectedName?: string;
is_sensitive?: boolean;
is_valid?: boolean;
};
import {
KeyboardFocusableComponent,
KeyboardFocusableComponentState,
} from "./keyboardFocusableComponent";
const SELECT_OPTION_EVENT = DropdownPositioner.USE_MOBILE_MODE
? "click"
: "pointerdown";
export class DropdownComponent extends ComponentBase {
declare state: Required<DropdownState>;
export type DropdownState = KeyboardFocusableComponentState & {
_type_: "Dropdown-builtin";
optionNames: string[];
label: string;
accessibility_label: string;
style: InputBoxStyle;
selectedName: string;
is_sensitive: boolean;
is_valid: boolean;
};
export class DropdownComponent extends KeyboardFocusableComponent<DropdownState> {
private inputBox: InputBox;
private hiddenOptionsElement: HTMLElement;
private popupElement: HTMLElement;
@@ -468,7 +470,7 @@ export class DropdownComponent extends ComponentBase {
}
updateElement(
deltaState: DropdownState,
deltaState: DeltaState<DropdownState>,
latentComponents: Set<ComponentBase>
): void {
super.updateElement(deltaState, latentComponents);

View File

@@ -1,15 +1,13 @@
import { applyIcon } from "../designApplication";
import { ComponentBase, ComponentState } from "./componentBase";
import { ComponentBase, ComponentState, DeltaState } from "./componentBase";
export type BuildFailedState = ComponentState & {
_type_: "BuildFailed-builtin";
export type ErrorPlaceholderState = ComponentState & {
_type_: "ErrorPlaceholder-builtin";
error_summary: string;
error_details: string;
};
export class ErrorPlaceholderComponent extends ComponentBase {
declare state: Required<BuildFailedState>;
export class ErrorPlaceholderComponent extends ComponentBase<ErrorPlaceholderState> {
private iconElement: HTMLElement;
private summaryElement: HTMLElement;
private detailsElement: HTMLElement;
@@ -55,7 +53,7 @@ export class ErrorPlaceholderComponent extends ComponentBase {
}
updateElement(
deltaState: BuildFailedState,
deltaState: DeltaState<ErrorPlaceholderState>,
latentComponents: Set<ComponentBase>
): void {
super.updateElement(deltaState, latentComponents);

View File

@@ -1,5 +1,5 @@
import { applyIcon, applySwitcheroo } from "../designApplication";
import { ComponentBase, ComponentState } from "./componentBase";
import { ComponentBase, ComponentState, DeltaState } from "./componentBase";
import { RippleEffect } from "../rippleEffect";
import { markEventAsHandled } from "../eventHandling";
import { ColorSetName, ComponentId } from "../dataModels";
@@ -113,19 +113,17 @@ function getFileIcon(filename: string): string {
type FilePickerAreaState = ComponentState & {
_type_: "FilePickerArea-builtin";
child_text?: string | null;
child_component?: ComponentId | null;
file_types?: string[];
multiple?: boolean;
files?: {
child_text: string | null;
child_component: ComponentId | null;
file_types: string[];
multiple: boolean;
files: {
id: string;
name: string;
}[];
};
export class FilePickerAreaComponent extends ComponentBase {
declare state: Required<FilePickerAreaState>;
export class FilePickerAreaComponent extends ComponentBase<FilePickerAreaState> {
private fileInput: HTMLInputElement;
private iconElement: HTMLElement;
private childContentContainer: HTMLElement;
@@ -285,7 +283,7 @@ export class FilePickerAreaComponent extends ComponentBase {
}
updateElement(
deltaState: FilePickerAreaState,
deltaState: DeltaState<FilePickerAreaState>,
latentComponents: Set<ComponentBase>
): void {
super.updateElement(deltaState, latentComponents);

View File

@@ -1,18 +1,16 @@
import { componentsById } from "../componentManagement";
import { ComponentId } from "../dataModels";
import { ComponentBase, ComponentState } from "./componentBase";
import { ComponentBase, ComponentState, DeltaState } from "./componentBase";
export type FlowState = ComponentState & {
_type_: "FlowContainer-builtin";
children?: ComponentId[];
row_spacing?: number;
column_spacing?: number;
justify?: "left" | "center" | "right" | "justify" | "grow";
children: ComponentId[];
row_spacing: number;
column_spacing: number;
justify: "left" | "center" | "right" | "justify" | "grow";
};
export class FlowComponent extends ComponentBase {
declare state: Required<FlowState>;
export class FlowComponent extends ComponentBase<FlowState> {
private innerElement: HTMLElement;
createElement(): HTMLElement {
@@ -27,7 +25,7 @@ export class FlowComponent extends ComponentBase {
}
updateElement(
deltaState: FlowState,
deltaState: DeltaState<FlowState>,
latentComponents: Set<ComponentBase>
): void {
super.updateElement(deltaState, latentComponents);

View File

@@ -4,7 +4,7 @@ import { ComponentId } from "../dataModels";
import { Debouncer } from "../debouncer";
import { callRemoteMethodDiscardResponse } from "../rpc";
import { getAllocatedHeightInPx, getAllocatedWidthInPx } from "../utils";
import { ComponentBase, ComponentState } from "./componentBase";
import { ComponentBase, ComponentState, DeltaState } from "./componentBase";
let notifyBackendOfWindowSizeChange = new Debouncer({
callback: (width: number, height: number) => {
@@ -26,9 +26,7 @@ export type FundamentalRootComponentState = ComponentState & {
dev_tools: ComponentId | null;
};
export class FundamentalRootComponent extends ComponentBase {
declare state: Required<FundamentalRootComponentState>;
export class FundamentalRootComponent extends ComponentBase<FundamentalRootState> {
private userRootContainer: HTMLElement;
public userOverlaysContainer: HTMLElement;
@@ -127,7 +125,7 @@ export class FundamentalRootComponent extends ComponentBase {
}
updateElement(
deltaState: FundamentalRootComponentState,
deltaState: DeltaState<FundamentalRootComponentState>,
latentComponents: Set<ComponentBase>
): void {
super.updateElement(deltaState, latentComponents);

View File

@@ -1,5 +1,5 @@
import { ComponentId } from "../../dataModels";
import { ComponentBase, ComponentState } from "../componentBase";
import { ComponentBase, ComponentState, DeltaState } from "../componentBase";
import { NodeInputComponent } from "../nodeInput";
import {
AugmentedConnectionState,
@@ -22,12 +22,10 @@ import { CuttingConnectionStrategy } from "./cuttingConnectionStrategy";
export type GraphEditorState = ComponentState & {
_type_: "GraphEditor-builtin";
children?: ComponentId[];
children: ComponentId[];
};
export class GraphEditorComponent extends ComponentBase {
declare state: Required<GraphEditorState>;
export class GraphEditorComponent extends ComponentBase<GraphEditorState> {
private htmlChild: HTMLElement;
public svgChild: SVGSVGElement;
@@ -83,7 +81,7 @@ export class GraphEditorComponent extends ComponentBase {
}
updateElement(
deltaState: GraphEditorState,
deltaState: DeltaState<GraphEditorState>,
latentComponents: Set<ComponentBase>
): void {
super.updateElement(deltaState, latentComponents);

View File

@@ -1,7 +1,7 @@
import { componentsById } from "../componentManagement";
import { ComponentId } from "../dataModels";
import { range, zip } from "../utils";
import { ComponentBase, ComponentState } from "./componentBase";
import { ComponentBase, ComponentState, DeltaState } from "./componentBase";
type GridChildPosition = {
row: number;
@@ -12,15 +12,13 @@ type GridChildPosition = {
export type GridState = ComponentState & {
_type_: "Grid-builtin";
_children?: ComponentId[];
_child_positions?: GridChildPosition[];
row_spacing?: number;
column_spacing?: number;
_children: ComponentId[];
_child_positions: GridChildPosition[];
row_spacing: number;
column_spacing: number;
};
export class GridComponent extends ComponentBase {
declare state: Required<GridState>;
export class GridComponent extends ComponentBase<GridState> {
createElement(): HTMLElement {
let element = document.createElement("div");
element.classList.add("rio-grid");
@@ -28,7 +26,7 @@ export class GridComponent extends ComponentBase {
}
updateElement(
deltaState: GridState,
deltaState: DeltaState<GridState>,
latentComponents: Set<ComponentBase>
): void {
super.updateElement(deltaState, latentComponents);

View File

@@ -1,14 +1,12 @@
import { ComponentBase, ComponentState } from "./componentBase";
import { ComponentBase, ComponentState, DeltaState } from "./componentBase";
import { textStyleToCss } from "../cssUtils";
export type HeadingListItemState = ComponentState & {
_type_: "HeadingListItem-builtin";
text?: string;
text: string;
};
export class HeadingListItemComponent extends ComponentBase {
declare state: Required<HeadingListItemState>;
export class HeadingListItemComponent extends ComponentBase<HeadingListItemState> {
createElement(): HTMLElement {
// Create the element
let element = document.createElement("div");
@@ -23,7 +21,7 @@ export class HeadingListItemComponent extends ComponentBase {
}
updateElement(
deltaState: HeadingListItemState,
deltaState: DeltaState<HeadingListItemState>,
latentComponents: Set<ComponentBase>
): void {
super.updateElement(deltaState, latentComponents);

View File

@@ -1,14 +1,12 @@
import { ComponentBase, ComponentState } from "./componentBase";
import { ComponentBase, ComponentState, DeltaState } from "./componentBase";
import { ComponentId } from "../dataModels";
export type HighLevelComponentState = ComponentState & {
_type_: "HighLevelComponent-builtin";
_child_?: ComponentId;
_child_: ComponentId;
};
export class HighLevelComponent extends ComponentBase {
declare state: Required<HighLevelComponentState>;
export class HighLevelComponent extends ComponentBase<HighLevelState> {
createElement(): HTMLElement {
let element = document.createElement("div");
element.classList.add("rio-high-level-component");
@@ -16,7 +14,7 @@ export class HighLevelComponent extends ComponentBase {
}
updateElement(
deltaState: HighLevelComponentState,
deltaState: DeltaState<HighLevelComponentState>,
latentComponents: Set<ComponentBase>
): void {
super.updateElement(deltaState, latentComponents);

View File

@@ -6,7 +6,7 @@ import {
RadialGradientFill,
SolidFill,
} from "../dataModels";
import { ComponentBase, ComponentState } from "./componentBase";
import { ComponentBase, ComponentState, DeltaState } from "./componentBase";
import { applyIcon, applyFillToSVG } from "../designApplication";
export type IconState = ComponentState & {
@@ -22,9 +22,7 @@ export type IconState = ComponentState & {
| "dim";
};
export class IconComponent extends ComponentBase {
declare state: Required<IconState>;
export class IconComponent extends ComponentBase<IconState> {
private svgElement: SVGSVGElement;
createElement(): HTMLElement {
@@ -34,7 +32,7 @@ export class IconComponent extends ComponentBase {
}
updateElement(
deltaState: IconState,
deltaState: DeltaState<IconState>,
latentComponents: Set<ComponentBase>
): void {
super.updateElement(deltaState, latentComponents);

View File

@@ -1,6 +1,6 @@
import { applyIcon } from "../designApplication";
import { getAllocatedHeightInPx, getAllocatedWidthInPx } from "../utils";
import { ComponentBase, ComponentState } from "./componentBase";
import { ComponentBase, ComponentState, DeltaState } from "./componentBase";
const FILL_MODE_TO_OBJECT_FIT = {
fit: "contain",
@@ -10,16 +10,14 @@ const FILL_MODE_TO_OBJECT_FIT = {
export type ImageState = ComponentState & {
_type_: "Image-builtin";
fill_mode?: keyof typeof FILL_MODE_TO_OBJECT_FIT;
imageUrl?: string;
reportError?: boolean;
corner_radius?: [number, number, number, number];
accessibility_description?: string;
fill_mode: keyof typeof FILL_MODE_TO_OBJECT_FIT;
imageUrl: string;
reportError: boolean;
corner_radius: [number, number, number, number];
accessibility_description: string;
};
export class ImageComponent extends ComponentBase {
declare state: Required<ImageState>;
export class ImageComponent extends ComponentBase<ImageState> {
private imageElement: HTMLImageElement;
private isLoading: boolean = false;
private resizeObserver: ResizeObserver;
@@ -49,7 +47,7 @@ export class ImageComponent extends ComponentBase {
}
updateElement(
deltaState: ImageState,
deltaState: DeltaState<ImageState>,
latentComponents: Set<ComponentBase>
): void {
super.updateElement(deltaState, latentComponents);

View File

@@ -1,6 +1,10 @@
import { ComponentBase, ComponentState } from "./componentBase";
import { ComponentBase, DeltaState } from "./componentBase";
import { ComponentId } from "../dataModels";
import { markEventAsHandled } from "../eventHandling";
import {
KeyboardFocusableComponent,
KeyboardFocusableComponentState,
} from "./keyboardFocusableComponent";
// https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_code_values
const HARDWARE_KEY_MAP = {
@@ -454,7 +458,7 @@ const SOFTWARE_KEY_MAP = {
ColorF1Green: "color-f1-green",
ColorF2Yellow: "color-f2-yellow",
ColorF3Blue: "color-f3-blue",
ColorF4Grey: "color-f4-grey",
ColorF4Grey: "color-f4-gray",
ColorF5Brown: "color-f5-brown",
ClosedCaptionToggle: "closed-caption-toggle",
Dimmer: "dimmer",
@@ -686,17 +690,15 @@ function encodeEvent(event: KeyboardEvent): EncodedEvent {
type KeyCombination = SoftwareKey | SoftwareKey[];
export type KeyEventListenerState = ComponentState & {
export type KeyEventListenerState = KeyboardFocusableComponentState & {
_type_: "KeyEventListener-builtin";
content?: ComponentId;
reportKeyDown?: KeyCombination[] | true;
reportKeyUp?: KeyCombination[] | true;
reportKeyPress?: KeyCombination[] | true;
content: ComponentId;
reportKeyDown: KeyCombination[] | true;
reportKeyUp: KeyCombination[] | true;
reportKeyPress: KeyCombination[] | true;
};
export class KeyEventListenerComponent extends ComponentBase {
declare state: Required<KeyEventListenerState>;
export class KeyEventListenerComponent extends KeyboardFocusableComponent<KeyEventListenerState> {
private keyDownCombinations: Map<string, KeyCombination> | true;
private keyUpCombinations: Map<string, KeyCombination> | true;
private keyPressCombinations: Map<string, KeyCombination> | true;
@@ -709,7 +711,7 @@ export class KeyEventListenerComponent extends ComponentBase {
}
updateElement(
deltaState: KeyEventListenerState,
deltaState: DeltaState<KeyEventListenerState>,
latentComponents: Set<ComponentBase>
): void {
super.updateElement(deltaState, latentComponents);
@@ -804,10 +806,6 @@ export class KeyEventListenerComponent extends ComponentBase {
...encodedEvent,
});
}
grabKeyboardFocus(): void {
this.element.focus();
}
}
function keyCombinationsMapFromDeltaState(

View File

@@ -0,0 +1,45 @@
import { ComponentId } from "../dataModels";
import { ComponentBase, ComponentState } from "./componentBase";
export type KeyboardFocusableComponentState = ComponentState & {
auto_focus: boolean;
};
/// Base class for components that can receive keyboard focus. What this class
/// does:
/// - Enforces the presence of `auto_focus` in the component state
/// - Focuses the component on mount if `auto_focus` is true
///
/// There are also some other places where this class is used:
/// - In `updateComponentStates`, the keyboard focus is automatically moved to a
/// `KeyboardFocusableComponent` if the focused component dies
/// - The `setKeyboardFocus` RPC function only works with these components
export abstract class KeyboardFocusableComponent<
S extends KeyboardFocusableComponentState = KeyboardFocusableComponentState,
> extends ComponentBase<S> {
constructor(id: ComponentId, state: S) {
super(id, state);
// `.focus()` may not work on the initial page load (it's probably
// blocked unless there's user interaction like a click), so we'll
// use the `autofocus` attribute.
if (state.auto_focus) {
let element = this.getElementForKeyboardFocus();
element.autofocus = true;
}
// `autofocus` doesn't work in dialogs (probably because they open with
// a delay), so we'll add our own delay.
setTimeout(() => {
this.grabKeyboardFocus();
}, 100);
}
public grabKeyboardFocus(): void {
this.getElementForKeyboardFocus().focus();
}
protected getElementForKeyboardFocus(): HTMLElement {
return this.element;
}
}

View File

@@ -1,4 +1,4 @@
import { ComponentBase, ComponentState } from "./componentBase";
import { ComponentBase, ComponentState, DeltaState } from "./componentBase";
import { componentsById } from "../componentManagement";
import { getDisplayableChildren } from "../devToolsTreeWalk";
import { Highlighter } from "../highlighter";
@@ -9,13 +9,11 @@ import { getAllocatedHeightInPx, getAllocatedWidthInPx } from "../utils";
export type LayoutDisplayState = ComponentState & {
_type_: "LayoutDisplay-builtin";
component_id?: number;
max_requested_height?: number;
component_id: number;
max_requested_height: number;
};
export class LayoutDisplayComponent extends ComponentBase {
declare state: Required<LayoutDisplayState>;
export class LayoutDisplayComponent extends ComponentBase<LayoutDisplayState> {
// Represents the target component's parent. It matches the aspect ratio of
// the parent and is centered within this component.
parentElement: HTMLElement;
@@ -99,7 +97,7 @@ export class LayoutDisplayComponent extends ComponentBase {
}
updateElement(
deltaState: LayoutDisplayState,
deltaState: DeltaState<LayoutDisplayState>,
latentComponents: Set<ComponentBase>
): void {
super.updateElement(deltaState, latentComponents);

View File

@@ -7,13 +7,13 @@ import {
OnlyResizeObserver,
zip,
} from "../utils";
import { ComponentBase, ComponentState } from "./componentBase";
import { ComponentBase, ComponentState, DeltaState } from "./componentBase";
export type LinearContainerState = ComponentState & {
_type_: "Row-builtin" | "Column-builtin";
children?: ComponentId[];
spacing?: number;
proportions?: "homogeneous" | number[] | null;
children: ComponentId[];
spacing: number;
proportions: "homogeneous" | number[] | null;
};
// The size of the invisible spacer element. It must be large enough to account
@@ -23,9 +23,7 @@ export type LinearContainerState = ComponentState & {
// its job.)
const PROPORTIONS_SPACER_SIZE = 50;
export abstract class LinearContainer extends ComponentBase {
declare state: Required<LinearContainerState>;
export abstract class LinearContainer extends ComponentBase<LinearContainerState> {
index: 0 | 1; // 0 for Rows, 1 for Columns
sizeAttribute: "width" | "height"; // 'width' for Rows, 'height' for Columns
@@ -73,7 +71,7 @@ export abstract class LinearContainer extends ComponentBase {
}
updateElement(
deltaState: LinearContainerState,
deltaState: DeltaState<LinearContainerState>,
latentComponents: Set<ComponentBase>
): void {
super.updateElement(deltaState, latentComponents);

View File

@@ -1,20 +1,18 @@
import { ComponentId } from "../dataModels";
import { ComponentBase, ComponentState } from "./componentBase";
import { ComponentBase, ComponentState, DeltaState } from "./componentBase";
import { hijackLinkElement } from "../utils";
import { applyIcon } from "../designApplication";
export type LinkState = ComponentState & {
_type_: "Link-builtin";
child_text?: string | null;
child_component?: ComponentId | null;
icon?: string | null;
open_in_new_tab?: boolean;
child_text: string | null;
child_component: ComponentId | null;
icon: string | null;
open_in_new_tab: boolean;
targetUrl: string;
};
export class LinkComponent extends ComponentBase {
declare state: Required<LinkState>;
export class LinkComponent extends ComponentBase<LinkState> {
createElement(): HTMLElement {
let element = document.createElement("a");
element.classList.add("rio-link");
@@ -25,7 +23,7 @@ export class LinkComponent extends ComponentBase {
}
updateElement(
deltaState: LinkState,
deltaState: DeltaState<LinkState>,
latentComponents: Set<ComponentBase>
): void {
super.updateElement(deltaState, latentComponents);

View File

@@ -1,18 +1,16 @@
import { componentsByElement, componentsById } from "../componentManagement";
import { ComponentId } from "../dataModels";
import { ComponentBase, ComponentState } from "./componentBase";
import { ComponentBase, ComponentState, DeltaState } from "./componentBase";
import { CustomListItemComponent } from "./customListItem";
import { HeadingListItemComponent } from "./headingListItem";
import { SeparatorListItemComponent } from "./separatorListItem";
export type ListViewState = ComponentState & {
_type_: "ListView-builtin";
children?: ComponentId[];
children: ComponentId[];
};
export class ListViewComponent extends ComponentBase {
declare state: Required<ListViewState>;
export class ListViewComponent extends ComponentBase<ListViewState> {
createElement(): HTMLElement {
let element = document.createElement("div");
element.classList.add("rio-list-view");
@@ -20,7 +18,7 @@ export class ListViewComponent extends ComponentBase {
}
updateElement(
deltaState: ListViewState,
deltaState: DeltaState<ListViewState>,
latentComponents: Set<ComponentBase>
): void {
super.updateElement(deltaState, latentComponents);

View File

@@ -1,4 +1,4 @@
import { ComponentBase, ComponentState } from "./componentBase";
import { ComponentBase, ComponentState, DeltaState } from "./componentBase";
import { micromark } from "micromark";
// This import decides which languages are supported by `highlight.js`. See
@@ -12,13 +12,13 @@ import { convertDivToCodeBlock } from "./codeBlock";
export type MarkdownState = ComponentState & {
_type_: "Markdown-builtin";
text?: string;
default_language?: null | string;
selectable?: boolean;
justify?: "left" | "right" | "center" | "justify";
overflow?: "nowrap" | "wrap" | "ellipsize";
scroll_code_x?: "never" | "auto" | "always";
scroll_code_y?: "never" | "auto" | "always";
text: string;
default_language: null | string;
selectable: boolean;
justify: "left" | "right" | "center" | "justify";
overflow: "nowrap" | "wrap" | "ellipsize";
scroll_code_x: "never" | "auto" | "always";
scroll_code_y: "never" | "auto" | "always";
};
// Convert a Markdown string to HTML and render it in the given div.
@@ -120,9 +120,7 @@ function hijackLocalLinks(div: HTMLElement): void {
}
}
export class MarkdownComponent extends ComponentBase {
declare state: Required<MarkdownState>;
export class MarkdownComponent extends ComponentBase<MarkdownState> {
createElement(): HTMLElement {
const element = document.createElement("div");
element.classList.add("rio-markdown");
@@ -130,7 +128,7 @@ export class MarkdownComponent extends ComponentBase {
}
updateElement(
deltaState: MarkdownState,
deltaState: DeltaState<MarkdownState>,
latentComponents: Set<ComponentBase>
): void {
super.updateElement(deltaState, latentComponents);

View File

@@ -2,20 +2,24 @@ import { fillToCss } from "../cssUtils";
import { applyIcon } from "../designApplication";
import { AnyFill } from "../dataModels";
import { sleep } from "../utils";
import { ComponentBase, ComponentState } from "./componentBase";
import { ComponentBase, DeltaState } from "./componentBase";
import { markEventAsHandled } from "../eventHandling";
import {
KeyboardFocusableComponent,
KeyboardFocusableComponentState,
} from "./keyboardFocusableComponent";
export type MediaPlayerState = ComponentState & {
export type MediaPlayerState = KeyboardFocusableComponentState & {
_type_: "MediaPlayer-builtin";
loop?: boolean;
autoplay?: boolean;
controls?: boolean;
muted?: boolean;
volume?: number;
mediaUrl?: string;
loop: boolean;
autoplay: boolean;
controls: boolean;
muted: boolean;
volume: number;
mediaUrl: string;
background: AnyFill;
reportError?: boolean;
reportPlaybackEnd?: boolean;
reportError: boolean;
reportPlaybackEnd: boolean;
};
const OVERLAY_TIMEOUT = 2000;
@@ -58,9 +62,7 @@ async function hasAudio(element: HTMLMediaElement): Promise<boolean> {
return true;
}
export class MediaPlayerComponent extends ComponentBase {
declare state: Required<MediaPlayerState>;
export class MediaPlayerComponent extends KeyboardFocusableComponent<MediaPlayerState> {
private mediaPlayer: HTMLVideoElement;
private altDisplay: HTMLElement;
private controls: HTMLElement;
@@ -566,7 +568,7 @@ export class MediaPlayerComponent extends ComponentBase {
}
updateElement(
deltaState: MediaPlayerState,
deltaState: DeltaState<MediaPlayerState>,
latentComponents: Set<ComponentBase>
): void {
super.updateElement(deltaState, latentComponents);
@@ -849,10 +851,6 @@ export class MediaPlayerComponent extends ComponentBase {
markEventAsHandled(event);
}
grabKeyboardFocus(): void {
this.element.focus();
}
private _onError(event: string | Event): void {
this.sendMessageToBackend({
type: "error",
@@ -874,4 +872,8 @@ export class MediaPlayerComponent extends ComponentBase {
this.mediaPlayer.src = "";
this.mediaPlayer.load();
}
protected override getElementForKeyboardFocus(): HTMLElement {
return this.mediaPlayer;
}
}

View File

@@ -1,7 +1,6 @@
import { pixelsPerRem } from "../app";
import { ComponentBase, ComponentState } from "./componentBase";
import { ComponentBase, ComponentState, DeltaState } from "./componentBase";
import { DragHandler } from "../eventHandling";
import { tryGetComponentByElement } from "../componentManagement";
import { ComponentId } from "../dataModels";
import { findComponentUnderMouse } from "../utils";
@@ -20,7 +19,7 @@ function eventMousePositionToString(event: MouseEvent): object {
export type MouseEventListenerState = ComponentState & {
_type_: "MouseEventListener-builtin";
content?: ComponentId;
content: ComponentId;
reportPress: boolean;
reportMouseDown: boolean;
reportMouseUp: boolean;
@@ -32,9 +31,7 @@ export type MouseEventListenerState = ComponentState & {
reportDragEnd: boolean;
};
export class MouseEventListenerComponent extends ComponentBase {
declare state: Required<MouseEventListenerState>;
export class MouseEventListenerComponent extends ComponentBase<MouseEventListenerState> {
private _dragHandler: DragHandler | null = null;
createElement(): HTMLElement {
@@ -44,7 +41,7 @@ export class MouseEventListenerComponent extends ComponentBase {
}
updateElement(
deltaState: MouseEventListenerState,
deltaState: DeltaState<MouseEventListenerState>,
latentComponents: Set<ComponentBase>
): void {
super.updateElement(deltaState, latentComponents);

View File

@@ -1,21 +1,23 @@
import { markEventAsHandled } from "../eventHandling";
import { InputBox, InputBoxStyle } from "../inputBox";
import { ComponentBase, ComponentState } from "./componentBase";
import { ComponentBase, DeltaState } from "./componentBase";
import {
KeyboardFocusableComponent,
KeyboardFocusableComponentState,
} from "./keyboardFocusableComponent";
export type MultiLineTextInputState = ComponentState & {
export type MultiLineTextInputState = KeyboardFocusableComponentState & {
_type_: "MultiLineTextInput-builtin";
text?: string;
label?: string;
accessibility_label?: string;
style?: InputBoxStyle;
is_sensitive?: boolean;
is_valid?: boolean;
auto_adjust_height?: boolean;
text: string;
label: string;
accessibility_label: string;
style: InputBoxStyle;
is_sensitive: boolean;
is_valid: boolean;
auto_adjust_height: boolean;
};
export class MultiLineTextInputComponent extends ComponentBase {
declare state: Required<MultiLineTextInputState>;
export class MultiLineTextInputComponent extends KeyboardFocusableComponent<MultiLineTextInputState> {
private inputBox: InputBox;
createElement(): HTMLElement {
@@ -79,7 +81,7 @@ export class MultiLineTextInputComponent extends ComponentBase {
}
updateElement(
deltaState: MultiLineTextInputState,
deltaState: DeltaState<MultiLineTextInputState>,
latentComponents: Set<ComponentBase>
): void {
super.updateElement(deltaState, latentComponents);
@@ -122,7 +124,7 @@ export class MultiLineTextInputComponent extends ComponentBase {
textarea.style.minHeight = `${textarea.scrollHeight}px`;
}
grabKeyboardFocus(): void {
this.inputBox.focus();
protected override getElementForKeyboardFocus(): HTMLElement {
return this.inputBox.inputElement;
}
}

View File

@@ -1,4 +1,4 @@
import { ComponentBase, ComponentState } from "./componentBase";
import { ComponentBase, ComponentState, DeltaState } from "./componentBase";
import { Color } from "../dataModels";
import { colorToCssString } from "../cssUtils";
@@ -9,9 +9,7 @@ export type NodeInputState = ComponentState & {
key: string;
};
export class NodeInputComponent extends ComponentBase {
declare state: Required<NodeInputState>;
export class NodeInputComponent extends ComponentBase<NodeInputState> {
textElement: HTMLElement;
circleElement: HTMLElement;
@@ -39,7 +37,7 @@ export class NodeInputComponent extends ComponentBase {
}
updateElement(
deltaState: NodeInputState,
deltaState: DeltaState<NodeInputState>,
latentComponents: Set<ComponentBase>
): void {
super.updateElement(deltaState, latentComponents);

View File

@@ -1,4 +1,4 @@
import { ComponentBase, ComponentState } from "./componentBase";
import { ComponentBase, ComponentState, DeltaState } from "./componentBase";
import { Color } from "../dataModels";
import { colorToCssString } from "../cssUtils";
@@ -9,9 +9,7 @@ export type NodeOutputState = ComponentState & {
key: string;
};
export class NodeOutputComponent extends ComponentBase {
declare state: Required<NodeOutputState>;
export class NodeOutputComponent extends ComponentBase<NodeOutputState> {
textElement: HTMLElement;
circleElement: HTMLElement;
@@ -39,7 +37,7 @@ export class NodeOutputComponent extends ComponentBase {
}
updateElement(
deltaState: NodeOutputState,
deltaState: DeltaState<NodeOutputState>,
latentComponents: Set<ComponentBase>
): void {
super.updateElement(deltaState, latentComponents);

View File

@@ -1,15 +1,13 @@
import { ComponentId } from "../dataModels";
import { FullscreenPositioner, PopupManager } from "../popupManager";
import { ComponentBase, ComponentState } from "./componentBase";
import { ComponentBase, ComponentState, DeltaState } from "./componentBase";
export type OverlayState = ComponentState & {
_type_: "Overlay-builtin";
content?: ComponentId;
content: ComponentId;
};
export class OverlayComponent extends ComponentBase {
declare state: Required<OverlayState>;
export class OverlayComponent extends ComponentBase<OverlayState> {
private overlayContentElement: HTMLElement;
private popupManager: PopupManager;
@@ -42,7 +40,7 @@ export class OverlayComponent extends ComponentBase {
}
updateElement(
deltaState: OverlayState,
deltaState: DeltaState<OverlayState>,
latentComponents: Set<ComponentBase>
): void {
super.updateElement(deltaState, latentComponents);

View File

@@ -1,7 +1,7 @@
import { fillToCss } from "../cssUtils";
import { AnyFill } from "../dataModels";
import { getAllocatedHeightInPx, getAllocatedWidthInPx } from "../utils";
import { ComponentBase, ComponentState } from "./componentBase";
import { ComponentBase, ComponentState, DeltaState } from "./componentBase";
type PlotlyType = any;
@@ -19,12 +19,10 @@ type PlotState = ComponentState & {
_type_: "Plot-builtin";
plot: PlotlyPlot | MatplotlibPlot;
background: AnyFill | null;
corner_radius?: [number, number, number, number];
corner_radius: [number, number, number, number];
};
export class PlotComponent extends ComponentBase {
declare state: Required<PlotState>;
export class PlotComponent extends ComponentBase<PlotState> {
// I know this abstraction looks like overkill, but plotly does so much
// stuff with a time delay (loading plotly, setTimeout, resizeObserver, ...)
// that it's just a giant mess of race conditions if it's not all
@@ -38,7 +36,7 @@ export class PlotComponent extends ComponentBase {
}
updateElement(
deltaState: PlotState,
deltaState: DeltaState<PlotState>,
latentComponents: Set<ComponentBase>
): void {
super.updateElement(deltaState, latentComponents);

View File

@@ -1,5 +1,5 @@
import { pixelsPerRem } from "../app";
import { ComponentBase, ComponentState } from "./componentBase";
import { ComponentBase, ComponentState, DeltaState } from "./componentBase";
import { DragHandler } from "../eventHandling";
import { tryGetComponentByElement } from "../componentManagement";
import { ComponentId } from "../dataModels";
@@ -7,7 +7,7 @@ import { findComponentUnderMouse } from "../utils";
export type PointerEventListenerState = ComponentState & {
_type_: "PointerEventListener-builtin";
content?: ComponentId;
content: ComponentId;
reportPress: boolean;
reportPointerDown: boolean;
reportPointerUp: boolean;
@@ -19,9 +19,7 @@ export type PointerEventListenerState = ComponentState & {
reportDragEnd: boolean;
};
export class PointerEventListenerComponent extends ComponentBase {
declare state: Required<PointerEventListenerState>;
export class PointerEventListenerComponent extends ComponentBase<PointerEventListenerState> {
private _dragHandler: DragHandler | null = null;
createElement(): HTMLElement {
@@ -31,7 +29,7 @@ export class PointerEventListenerComponent extends ComponentBase {
}
updateElement(
deltaState: PointerEventListenerState,
deltaState: DeltaState<PointerEventListenerState>,
latentComponents: Set<ComponentBase>
): void {
super.updateElement(deltaState, latentComponents);

View File

@@ -1,6 +1,6 @@
import { applySwitcheroo } from "../designApplication";
import { ColorSet, ComponentId } from "../dataModels";
import { ComponentBase, ComponentState } from "./componentBase";
import { ComponentBase, ComponentState, DeltaState } from "./componentBase";
import {
DesktopDropdownPositioner,
getPositionerByName,
@@ -11,14 +11,14 @@ import { componentsById } from "../componentManagement";
export type PopupState = ComponentState & {
_type_: "Popup-builtin";
anchor?: ComponentId;
content?: ComponentId;
is_open?: boolean;
anchor: ComponentId;
content: ComponentId;
is_open: boolean;
modal: boolean;
user_closable: boolean;
color?: ColorSet | "none";
corner_radius?: number | [number, number, number, number];
position?:
color: ColorSet | "none";
corner_radius: number | [number, number, number, number];
position:
| "auto"
| "left"
| "top"
@@ -27,13 +27,11 @@ export type PopupState = ComponentState & {
| "center"
| "fullscreen"
| "dropdown";
alignment?: number;
gap?: number;
alignment: number;
gap: number;
};
export class PopupComponent extends ComponentBase {
declare state: Required<PopupState>;
export class PopupComponent extends ComponentBase<PopupState> {
private popupContentElement: HTMLElement;
private popupScrollerElement: HTMLElement;
@@ -77,7 +75,7 @@ export class PopupComponent extends ComponentBase {
}
updateElement(
deltaState: PopupState,
deltaState: DeltaState<PopupState>,
latentComponents: Set<ComponentBase>
): void {
super.updateElement(deltaState, latentComponents);

View File

@@ -1,17 +1,15 @@
import { ColorSet } from "../dataModels";
import { applySwitcheroo } from "../designApplication";
import { ComponentBase, ComponentState } from "./componentBase";
import { ComponentBase, ComponentState, DeltaState } from "./componentBase";
export type ProgressBarState = ComponentState & {
_type_: "ProgressBar-builtin";
progress?: number | null;
color?: ColorSet;
rounded?: boolean;
progress: number | null;
color: ColorSet;
rounded: boolean;
};
export class ProgressBarComponent extends ComponentBase {
declare state: Required<ProgressBarState>;
export class ProgressBarComponent extends ComponentBase<ProgressBarState> {
fillElement: HTMLElement;
createElement(): HTMLElement {
@@ -31,7 +29,7 @@ export class ProgressBarComponent extends ComponentBase {
}
updateElement(
deltaState: ProgressBarState,
deltaState: DeltaState<ProgressBarState>,
latentComponents: Set<ComponentBase>
): void {
super.updateElement(deltaState, latentComponents);

View File

@@ -1,16 +1,14 @@
import { applySwitcheroo } from "../designApplication";
import { ColorSet } from "../dataModels";
import { ComponentBase, ComponentState } from "./componentBase";
import { ComponentBase, ComponentState, DeltaState } from "./componentBase";
export type ProgressCircleState = ComponentState & {
_type_: "ProgressCircle-builtin";
progress?: number | null;
color?: ColorSet;
progress: number | null;
color: ColorSet;
};
export class ProgressCircleComponent extends ComponentBase {
declare state: Required<ProgressCircleState>;
export class ProgressCircleComponent extends ComponentBase<ProgressCircleState> {
createElement(): HTMLElement {
let element = document.createElement("div");
@@ -24,7 +22,7 @@ export class ProgressCircleComponent extends ComponentBase {
}
updateElement(
deltaState: ProgressCircleState,
deltaState: DeltaState<ProgressCircleState>,
latentComponents: Set<ComponentBase>
): void {
super.updateElement(deltaState, latentComponents);

View File

@@ -1,32 +1,32 @@
import { Color, ComponentId, AnyFill } from "../dataModels";
import { colorToCssString, fillToCss } from "../cssUtils";
import { ComponentBase, ComponentState } from "./componentBase";
import { ComponentBase, ComponentState, DeltaState } from "./componentBase";
import { RippleEffect } from "../rippleEffect";
export type RectangleState = ComponentState & {
_type_: "Rectangle-builtin";
content?: ComponentId | null;
transition_time?: number;
cursor?: string;
ripple?: boolean;
content: ComponentId | null;
transition_time: number;
cursor: string;
ripple: boolean;
fill?: AnyFill;
stroke_color?: Color;
stroke_width?: number;
corner_radius?: [number, number, number, number];
shadow_color?: Color;
shadow_radius?: number;
shadow_offset_x?: number;
shadow_offset_y?: number;
fill: AnyFill;
stroke_color: Color;
stroke_width: number;
corner_radius: [number, number, number, number];
shadow_color: Color;
shadow_radius: number;
shadow_offset_x: number;
shadow_offset_y: number;
hover_fill?: AnyFill | null;
hover_stroke_color?: Color | null;
hover_stroke_width?: number | null;
hover_corner_radius?: [number, number, number, number] | null;
hover_shadow_color?: Color | null;
hover_shadow_radius?: number | null;
hover_shadow_offset_x?: number | null;
hover_shadow_offset_y?: number | null;
hover_fill: AnyFill | null;
hover_stroke_color: Color | null;
hover_stroke_width: number | null;
hover_corner_radius: [number, number, number, number] | null;
hover_shadow_color: Color | null;
hover_shadow_radius: number | null;
hover_shadow_offset_x: number | null;
hover_shadow_offset_y: number | null;
};
function numberToRem(num: number): string {
@@ -71,9 +71,7 @@ function cursorToCSS(cursor: string): string {
return CURSOR_MAP[cursor];
}
export class RectangleComponent extends ComponentBase {
declare state: Required<RectangleState>;
export class RectangleComponent extends ComponentBase<RectangleState> {
// If this rectangle has a ripple effect, this is the ripple instance.
// `null` otherwise.
private rippleInstance: RippleEffect | null = null;
@@ -85,7 +83,7 @@ export class RectangleComponent extends ComponentBase {
}
updateElement(
deltaState: RectangleState,
deltaState: DeltaState<RectangleState>,
latentComponents: Set<ComponentBase>
): void {
super.updateElement(deltaState, latentComponents);

View File

@@ -2,22 +2,20 @@ import { textStyleToCss } from "../cssUtils";
import { applyIcon } from "../designApplication";
import { ComponentId, TextStyle } from "../dataModels";
import { commitCss } from "../utils";
import { ComponentBase, ComponentState } from "./componentBase";
import { ComponentBase, ComponentState, DeltaState } from "./componentBase";
import { RippleEffect } from "../rippleEffect";
let HEADER_PADDING: number = 0.3;
export type RevealerState = ComponentState & {
_type_: "Revealer-builtin";
header?: string | null;
content?: ComponentId;
header_style?: "heading1" | "heading2" | "heading3" | "text" | TextStyle;
header: string | null;
content: ComponentId;
header_style: "heading1" | "heading2" | "heading3" | "text" | TextStyle;
is_open: boolean;
};
export class RevealerComponent extends ComponentBase {
declare state: Required<RevealerState>;
export class RevealerComponent extends ComponentBase<RevealerState> {
private headerElement: HTMLElement;
private labelElement: HTMLElement;
private arrowElement: HTMLElement;
@@ -93,7 +91,7 @@ export class RevealerComponent extends ComponentBase {
}
updateElement(
deltaState: RevealerState,
deltaState: DeltaState<RevealerState>,
latentComponents: Set<ComponentBase>
): void {
super.updateElement(deltaState, latentComponents);

View File

@@ -1,20 +1,18 @@
import { ComponentId } from "../dataModels";
import { ComponentBase, ComponentState } from "./componentBase";
import { ComponentBase, ComponentState, DeltaState } from "./componentBase";
export type ScrollContainerState = ComponentState & {
_type_: "ScrollContainer-builtin";
content?: ComponentId;
scroll_x?: "never" | "auto" | "always";
scroll_y?: "never" | "auto" | "always";
initial_x?: number;
initial_y?: number;
reserve_space_y?: boolean;
sticky_bottom?: boolean;
content: ComponentId;
scroll_x: "never" | "auto" | "always";
scroll_y: "never" | "auto" | "always";
initial_x: number;
initial_y: number;
reserve_space_y: boolean;
sticky_bottom: boolean;
};
export class ScrollContainerComponent extends ComponentBase {
declare state: Required<ScrollContainerState>;
export class ScrollContainerComponent extends ComponentBase<ScrollContainerState> {
private scrollerElement: HTMLElement;
private childContainer: HTMLElement;
private scrollAnchor: HTMLElement;
@@ -59,7 +57,7 @@ export class ScrollContainerComponent extends ComponentBase {
}
updateElement(
deltaState: ScrollContainerState,
deltaState: DeltaState<ScrollContainerState>,
latentComponents: Set<ComponentBase>
): void {
super.updateElement(deltaState, latentComponents);

View File

@@ -1,20 +1,18 @@
import { tryGetComponentByElement } from "../componentManagement";
import { ComponentId } from "../dataModels";
import { setClipboard } from "../utils";
import { ComponentBase, ComponentState } from "./componentBase";
import { ComponentBase, ComponentState, DeltaState } from "./componentBase";
export type ScrollTargetState = ComponentState & {
_type_: "ScrollTarget-builtin";
id?: string;
content?: ComponentId | null;
copy_button_content?: ComponentId | null;
copy_button_text?: string | null;
copy_button_spacing?: number;
id: string;
content: ComponentId | null;
copy_button_content: ComponentId | null;
copy_button_text: string | null;
copy_button_spacing: number;
};
export class ScrollTargetComponent extends ComponentBase {
declare state: Required<ScrollTargetState>;
export class ScrollTargetComponent extends ComponentBase<ScrollTargetState> {
childContainerElement: HTMLElement;
buttonContainerElement: HTMLElement;
@@ -42,7 +40,7 @@ export class ScrollTargetComponent extends ComponentBase {
}
updateElement(
deltaState: ScrollTargetState,
deltaState: DeltaState<ScrollTargetState>,
latentComponents: Set<ComponentBase>
): void {
super.updateElement(deltaState, latentComponents);

View File

@@ -1,5 +1,5 @@
import { Color } from "../dataModels";
import { ComponentBase, ComponentState } from "./componentBase";
import { ComponentBase, ComponentState, DeltaState } from "./componentBase";
import { colorToCssString } from "../cssUtils";
export type SeparatorState = ComponentState & {
@@ -8,9 +8,7 @@ export type SeparatorState = ComponentState & {
color: Color;
};
export class SeparatorComponent extends ComponentBase {
declare state: Required<SeparatorState>;
export class SeparatorComponent extends ComponentBase<SeparatorState> {
createElement(): HTMLElement {
let element = document.createElement("div");
element.classList.add("rio-separator");
@@ -18,7 +16,7 @@ export class SeparatorComponent extends ComponentBase {
}
updateElement(
deltaState: SeparatorState,
deltaState: DeltaState<SeparatorState>,
latentComponents: Set<ComponentBase>
): void {
super.updateElement(deltaState, latentComponents);

View File

@@ -1,12 +1,10 @@
import { ComponentBase, ComponentState } from "./componentBase";
import { ComponentBase, ComponentState, DeltaState } from "./componentBase";
export type SeparatorListItemState = ComponentState & {
_type_: "SeparatorListItem-builtin";
};
export class SeparatorListItemComponent extends ComponentBase {
declare state: Required<SeparatorListItemState>;
export class SeparatorListItemComponent extends ComponentBase<SeparatorListItemState> {
createElement(): HTMLElement {
let element = document.createElement("div");
element.classList.add("rio-separator-list-item");

View File

@@ -1,21 +1,19 @@
import { applySwitcheroo } from "../designApplication";
import { markEventAsHandled } from "../eventHandling";
import { ComponentBase, ComponentState } from "./componentBase";
import { ComponentBase, ComponentState, DeltaState } from "./componentBase";
export type SliderState = ComponentState & {
_type_: "Slider-builtin";
minimum?: number;
maximum?: number;
value?: number;
step?: number;
is_sensitive?: boolean;
show_values?: boolean;
ticks?: (number | string | [number, string])[] | boolean;
minimum: number;
maximum: number;
value: number;
step: number;
is_sensitive: boolean;
show_values: boolean;
ticks: (number | string | [number, string])[] | boolean;
};
export class SliderComponent extends ComponentBase {
declare state: Required<SliderState>;
export class SliderComponent extends ComponentBase<SliderState> {
private innerElement: HTMLElement;
private minValueElement: HTMLElement;
private maxValueElement: HTMLElement;
@@ -138,7 +136,7 @@ export class SliderComponent extends ComponentBase {
}
updateElement(
deltaState: SliderState,
deltaState: DeltaState<SliderState>,
latentComponents: Set<ComponentBase>
): void {
super.updateElement(deltaState, latentComponents);

View File

@@ -1,4 +1,4 @@
import { ComponentBase, ComponentState } from "./componentBase";
import { ComponentBase, ComponentState, DeltaState } from "./componentBase";
import { easeIn, easeInOut, easeOut } from "../easeFunctions";
import { ComponentId } from "../dataModels";
@@ -7,15 +7,13 @@ const progressBarFadeDuration = 0.2;
export type SlideshowState = ComponentState & {
_type_: "Slideshow-builtin";
children?: ComponentId[];
linger_time?: number;
pause_on_hover?: boolean;
corner_radius?: [number, number, number, number];
children: ComponentId[];
linger_time: number;
pause_on_hover: boolean;
corner_radius: [number, number, number, number];
};
export class SlideshowComponent extends ComponentBase {
declare state: Required<SlideshowState>;
export class SlideshowComponent extends ComponentBase<SlideshowState> {
private childContainer: HTMLElement;
private progressBar: HTMLElement;
@@ -70,7 +68,7 @@ export class SlideshowComponent extends ComponentBase {
}
updateElement(
deltaState: SlideshowState,
deltaState: DeltaState<SlideshowState>,
latentComponents: Set<ComponentBase>
): void {
super.updateElement(deltaState, latentComponents);

View File

@@ -1,14 +1,12 @@
import { ComponentId } from "../dataModels";
import { ComponentBase, ComponentState } from "./componentBase";
import { ComponentBase, ComponentState, DeltaState } from "./componentBase";
export type StackState = ComponentState & {
_type_: "Stack-builtin";
children?: ComponentId[];
children: ComponentId[];
};
export class StackComponent extends ComponentBase {
declare state: Required<StackState>;
export class StackComponent extends ComponentBase<StackState> {
createElement(): HTMLElement {
let element = document.createElement("div");
element.classList.add("rio-stack");
@@ -16,7 +14,7 @@ export class StackComponent extends ComponentBase {
}
updateElement(
deltaState: StackState,
deltaState: DeltaState<StackState>,
latentComponents: Set<ComponentBase>
): void {
super.updateElement(deltaState, latentComponents);

View File

@@ -1,15 +1,13 @@
import { applyIcon } from "../designApplication";
import { ComponentBase, ComponentState } from "./componentBase";
import { ComponentBase, ComponentState, DeltaState } from "./componentBase";
export type SwitchState = ComponentState & {
_type_: "Switch-builtin";
is_on?: boolean;
is_sensitive?: boolean;
is_on: boolean;
is_sensitive: boolean;
};
export class SwitchComponent extends ComponentBase {
declare state: Required<SwitchState>;
export class SwitchComponent extends ComponentBase<SwitchState> {
createElement(): HTMLElement {
let element = document.createElement("div");
element.classList.add("rio-switch");
@@ -37,7 +35,7 @@ export class SwitchComponent extends ComponentBase {
}
updateElement(
deltaState: SwitchState,
deltaState: DeltaState<SwitchState>,
latentComponents: Set<ComponentBase>
): void {
super.updateElement(deltaState, latentComponents);

View File

@@ -1,17 +1,15 @@
import { ComponentId } from "../dataModels";
import { ComponentBase, ComponentState } from "./componentBase";
import { ComponentBase, ComponentState, DeltaState } from "./componentBase";
import { componentsById } from "../componentManagement";
import { commitCss } from "../utils";
export type SwitcherState = ComponentState & {
_type_: "Switcher-builtin";
content?: ComponentId | null;
transition_time?: number;
content: ComponentId | null;
transition_time: number;
};
export class SwitcherComponent extends ComponentBase {
declare state: Required<SwitcherState>;
export class SwitcherComponent extends ComponentBase<SwitcherState> {
private activeChildContainer: HTMLElement | null = null;
private resizerElement: HTMLElement | null = null;
private idOfCurrentAnimation: number = 0;
@@ -24,7 +22,7 @@ export class SwitcherComponent extends ComponentBase {
}
updateElement(
deltaState: SwitcherState,
deltaState: DeltaState<SwitcherState>,
latentComponents: Set<ComponentBase>
): void {
super.updateElement(deltaState, latentComponents);

View File

@@ -1,4 +1,4 @@
import { ComponentBase, ComponentState } from "./componentBase";
import { ComponentBase, ComponentState, DeltaState } from "./componentBase";
import { ColorSet } from "../dataModels";
import { applyIcon, applySwitcheroo } from "../designApplication";
import { MappingTween } from "../tweens/mappingTweens";
@@ -13,18 +13,16 @@ import {
export type SwitcherBarState = ComponentState & {
_type_: "SwitcherBar-builtin";
names?: string[];
icons?: (string | null)[] | null;
color?: ColorSet;
orientation?: "horizontal" | "vertical";
spacing?: number;
names: string[];
icons: (string | null)[] | null;
color: ColorSet;
orientation: "horizontal" | "vertical";
spacing: number;
allow_none: boolean;
selectedName?: string | null;
selectedName: string | null;
};
export class SwitcherBarComponent extends ComponentBase {
declare state: Required<SwitcherBarState>;
export class SwitcherBarComponent extends ComponentBase<SwitcherBarState> {
private innerElement: HTMLElement; // Used for alignment
private markerElement: HTMLElement; // Highlights the selected item
private backgroundOptionsElement: HTMLElement; // Displays all options
@@ -259,7 +257,7 @@ export class SwitcherBarComponent extends ComponentBase {
event.stopPropagation();
}
buildContent(deltaState: SwitcherBarState): HTMLElement {
buildContent(deltaState: DeltaState<SwitcherBarState>): HTMLElement {
let result = document.createElement("div");
result.classList.add("rio-switcher-bar-options");
result.style.gap = `${this.state.spacing}rem`;
@@ -299,7 +297,7 @@ export class SwitcherBarComponent extends ComponentBase {
}
updateElement(
deltaState: SwitcherBarState,
deltaState: DeltaState<SwitcherBarState>,
latentComponents: Set<ComponentBase>
): void {
super.updateElement(deltaState, latentComponents);

View File

@@ -1,7 +1,6 @@
import { colorToCssString } from "../cssUtils";
import { Color } from "../dataModels";
import { markEventAsHandled } from "../eventHandling";
import { ComponentBase, ComponentState } from "./componentBase";
import { ComponentBase, ComponentState, DeltaState } from "./componentBase";
type TableValue = number | string;
@@ -11,23 +10,21 @@ type TableStyle = {
width: number;
height: number;
fontColor?: Color;
backgroundColor?: Color;
italic?: boolean;
fontWeight?: "normal" | "bold";
fontColor: Color;
backgroundColor: Color;
italic: boolean;
fontWeight: "normal" | "bold";
};
type TableState = ComponentState & {
_type_: "Table-builtin";
show_row_numbers?: boolean;
headers?: string[] | null;
columns?: TableValue[][];
styling?: TableStyle[];
show_row_numbers: boolean;
headers: string[] | null;
columns: TableValue[][];
styling: TableStyle[];
};
export class TableComponent extends ComponentBase {
declare state: Required<TableState>;
export class TableComponent extends ComponentBase<TableState> {
private dataWidth: number;
private dataHeight: number;
@@ -66,7 +63,7 @@ export class TableComponent extends ComponentBase {
}
updateElement(
deltaState: TableState,
deltaState: DeltaState<TableState>,
latentComponents: Set<ComponentBase>
): void {
super.updateElement(deltaState, latentComponents);

View File

@@ -6,35 +6,33 @@ import {
TextStyle,
} from "../dataModels";
import { textfillToCss, textStyleToCss } from "../cssUtils";
import { ComponentBase, ComponentState } from "./componentBase";
import { ComponentBase, ComponentState, DeltaState } from "./componentBase";
export type TextState = ComponentState & {
_type_: "Text-builtin";
text?: string;
selectable?: boolean;
style?: "heading1" | "heading2" | "heading3" | "text" | "dim" | TextStyle;
justify?: "left" | "right" | "center" | "justify";
overflow?: "nowrap" | "wrap" | "ellipsize";
text: string;
selectable: boolean;
style: "heading1" | "heading2" | "heading3" | "text" | "dim" | TextStyle;
justify: "left" | "right" | "center" | "justify";
overflow: "nowrap" | "wrap" | "ellipsize";
font?: string | null;
fill?:
font: string | null;
fill:
| Color
| SolidFill
| LinearGradientFill
| ImageFill
| null
| "not given";
font_size?: number | null;
italic?: boolean | null;
font_weight?: "normal" | "bold" | null;
underlined?: boolean | null;
strikethrough?: boolean | null;
all_caps?: boolean | null;
font_size: number | null;
italic: boolean | null;
font_weight: "normal" | "bold" | null;
underlined: boolean | null;
strikethrough: boolean | null;
all_caps: boolean | null;
};
export class TextComponent extends ComponentBase {
declare state: Required<TextState>;
export class TextComponent extends ComponentBase<TextState> {
private inner: HTMLElement;
createElement(): HTMLElement {
@@ -48,7 +46,7 @@ export class TextComponent extends ComponentBase {
}
updateElement(
deltaState: TextState,
deltaState: DeltaState<TextState>,
latentComponents: Set<ComponentBase>
): void {
super.updateElement(deltaState, latentComponents);

View File

@@ -1,30 +1,36 @@
import { ComponentBase, ComponentState } from "./componentBase";
import { ComponentBase, DeltaState } from "./componentBase";
import { Debouncer } from "../debouncer";
import { InputBox, InputBoxStyle } from "../inputBox";
import { markEventAsHandled } from "../eventHandling";
import {
KeyboardFocusableComponent,
KeyboardFocusableComponentState,
} from "./keyboardFocusableComponent";
export type TextInputState = ComponentState & {
export type TextInputState = KeyboardFocusableComponentState & {
_type_: "TextInput-builtin";
text?: string;
label?: string;
accessibility_label?: string;
style?: InputBoxStyle;
prefix_text?: string;
suffix_text?: string;
is_secret?: boolean;
is_sensitive?: boolean;
is_valid?: boolean;
text: string;
label: string;
accessibility_label: string;
style: InputBoxStyle;
prefix_text: string;
suffix_text: string;
is_secret: boolean;
is_sensitive: boolean;
is_valid: boolean;
};
export class TextInputComponent extends ComponentBase {
declare state: Required<TextInputState>;
export class TextInputComponent extends KeyboardFocusableComponent<TextInputState> {
private inputBox: InputBox;
private onChangeLimiter: Debouncer;
createElement(): HTMLElement {
this.inputBox = new InputBox();
if (this.state.auto_focus) {
this.inputBox.inputElement.autofocus = true;
}
let element = this.inputBox.outerElement;
// Create a rate-limited function for notifying the backend of changes.
@@ -115,7 +121,7 @@ export class TextInputComponent extends ComponentBase {
}
updateElement(
deltaState: TextInputState,
deltaState: DeltaState<TextInputState>,
latentComponents: Set<ComponentBase>
): void {
super.updateElement(deltaState, latentComponents);
@@ -159,7 +165,7 @@ export class TextInputComponent extends ComponentBase {
}
}
grabKeyboardFocus(): void {
this.inputBox.focus();
protected override getElementForKeyboardFocus(): HTMLElement {
return this.inputBox.inputElement;
}
}

View File

@@ -1,16 +1,14 @@
import { applySwitcheroo } from "../designApplication";
import { ColorSet, ComponentId } from "../dataModels";
import { ComponentBase, ComponentState } from "./componentBase";
import { ComponentBase, ComponentState, DeltaState } from "./componentBase";
export type ThemeContextSwitcherState = ComponentState & {
_type_: "ThemeContextSwitcher-builtin";
content?: ComponentId;
color?: ColorSet;
content: ComponentId;
color: ColorSet;
};
export class ThemeContextSwitcherComponent extends ComponentBase {
declare state: Required<ThemeContextSwitcherState>;
export class ThemeContextSwitcherComponent extends ComponentBase<ThemeContextSwitcherState> {
createElement(): HTMLElement {
let element = document.createElement("div");
element.classList.add("rio-single-container");
@@ -18,7 +16,7 @@ export class ThemeContextSwitcherComponent extends ComponentBase {
}
updateElement(
deltaState: ThemeContextSwitcherState,
deltaState: DeltaState<ThemeContextSwitcherState>,
latentComponents: Set<ComponentBase>
): void {
super.updateElement(deltaState, latentComponents);

View File

@@ -1,18 +1,16 @@
import { ComponentId } from "../dataModels";
import { ComponentBase, ComponentState } from "./componentBase";
import { ComponentBase, ComponentState, DeltaState } from "./componentBase";
import { getPositionerByName, PopupManager } from "../popupManager";
export type TooltipState = ComponentState & {
_type_: "Tooltip-builtin";
anchor?: ComponentId;
_tip_component?: ComponentId | null;
position?: "auto" | "left" | "top" | "right" | "bottom";
gap?: number;
anchor: ComponentId;
_tip_component: ComponentId | null;
position: "auto" | "left" | "top" | "right" | "bottom";
gap: number;
};
export class TooltipComponent extends ComponentBase {
declare state: Required<TooltipState>;
export class TooltipComponent extends ComponentBase<TooltipState> {
private popupElement: HTMLElement;
private popupManager: PopupManager;
@@ -51,7 +49,7 @@ export class TooltipComponent extends ComponentBase {
}
updateElement(
deltaState: TooltipState,
deltaState: DeltaState<TooltipState>,
latentComponents: Set<ComponentBase>
): void {
super.updateElement(deltaState, latentComponents);

View File

@@ -1,15 +1,13 @@
import { ComponentBase, ComponentState } from "./componentBase";
import { ComponentBase, ComponentState, DeltaState } from "./componentBase";
export type WebviewState = ComponentState & {
_type_: "Webview-builtin";
content?: string; // Url or Html code
enable_pointer_events?: boolean;
resize_to_fit_content?: boolean;
content: string; // Url or Html code
enable_pointer_events: boolean;
resize_to_fit_content: boolean;
};
export class WebviewComponent extends ComponentBase {
declare state: Required<WebviewState>;
export class WebviewComponent extends ComponentBase<WebviewState> {
private iframe: HTMLIFrameElement | null = null;
private resizeObserver: ResizeObserver | null = null;
private isInitialized = false;
@@ -21,7 +19,7 @@ export class WebviewComponent extends ComponentBase {
}
updateElement(
deltaState: WebviewState,
deltaState: DeltaState<WebviewState>,
latentComponents: Set<ComponentBase>
): void {
super.updateElement(deltaState, latentComponents);

View File

@@ -1,5 +1,6 @@
import { goingAway, pixelsPerRem } from "./app";
import { componentsById, updateComponentStates } from "./componentManagement";
import { KeyboardFocusableComponent } from "./components/keyboardFocusableComponent";
import {
requestFileUpload,
registerFont,
@@ -386,8 +387,10 @@ export async function processMessageReturnResponse(
case "setKeyboardFocus":
let component = componentsById[message.params.component_id]!;
// @ts-expect-error
component.grabKeyboardFocus();
if (component instanceof KeyboardFocusableComponent) {
component.grabKeyboardFocus();
}
response = null;
break;

View File

@@ -9,7 +9,7 @@ from uniserde import JsonDoc
import rio
from .. import utils
from .fundamental_component import FundamentalComponent
from .keyboard_focusable_components import KeyboardFocusableFundamentalComponent
__all__ = [
"Dropdown",
@@ -39,7 +39,7 @@ class DropdownChangeEvent(t.Generic[T]):
@t.final
class Dropdown(FundamentalComponent, t.Generic[T]):
class Dropdown(KeyboardFocusableFundamentalComponent, t.Generic[T]):
"""
A dropdown menu for selecting from one of several options.
@@ -248,9 +248,9 @@ class Dropdown(FundamentalComponent, t.Generic[T]):
)
# Trigger the event
assert not isinstance(
self.selected_value, utils.NotGiven
), self.selected_value
assert not isinstance(self.selected_value, utils.NotGiven), (
self.selected_value
)
await self.call_event_handler(
self.on_change, DropdownChangeEvent(self.selected_value)

View File

@@ -10,10 +10,7 @@ import rio
from .. import inspection, utils
from .component import Component
__all__ = [
"FundamentalComponent",
"KeyboardFocusableFundamentalComponent",
]
__all__ = ["FundamentalComponent"]
JAVASCRIPT_SOURCE_TEMPLATE = """
@@ -181,19 +178,3 @@ class FundamentalComponent(Component):
if not was_already_dirty:
self.session._dirty_components.discard(self)
class KeyboardFocusableFundamentalComponent(FundamentalComponent):
"""
## Metadata
`public`: False
"""
async def grab_keyboard_focus(self) -> None:
"""
## Metadata
`public`: False
"""
await self.session._remote_set_keyboard_focus(self._id_)

View File

@@ -8,7 +8,7 @@ from uniserde import Jsonable
import rio
from .fundamental_component import KeyboardFocusableFundamentalComponent
from .keyboard_focusable_components import KeyboardFocusableFundamentalComponent
__all__ = [
"KeyEventListener",
@@ -438,7 +438,7 @@ SoftwareKey = t.Literal[
"color-f1-green",
"color-f2-yellow",
"color-f3-blue",
"color-f4-grey",
"color-f4-gray",
"color-f5-brown",
"closed-caption-toggle",
"dimmer",
@@ -569,7 +569,7 @@ SoftwareKey = t.Literal[
ModifierKey = t.Literal["alt", "control", "meta", "shift"]
KeyCombination = SoftwareKey | tuple[ModifierKey | SoftwareKey, ...]
_MODIFIERS = ("control", "shift", "alt", "meta")
_MODIFIERS: tuple[ModifierKey, ...] = t.get_args(ModifierKey)
@dataclasses.dataclass(frozen=True)

View File

@@ -0,0 +1,47 @@
import abc
import dataclasses
from .component import Component
from .fundamental_component import FundamentalComponent
__all__ = [
"KeyboardFocusableComponent",
"KeyboardFocusableFundamentalComponent",
]
class KeyboardFocusableComponent(Component, abc.ABC):
"""
## Attributes
`auto_focus`: Whether this component should receive the keyboard focus when
it is created.
## Metadata
`public`: False
"""
_: dataclasses.KW_ONLY
auto_focus: bool = False
@abc.abstractmethod
async def grab_keyboard_focus(self) -> None:
"""
## Metadata
`public`: False
"""
raise NotImplementedError
class KeyboardFocusableFundamentalComponent(
KeyboardFocusableComponent, FundamentalComponent
):
async def grab_keyboard_focus(self) -> None:
"""
## Metadata
`public`: False
"""
await self.session._remote_set_keyboard_focus(self._id_)

View File

@@ -9,7 +9,7 @@ import rio
from .. import assets, color, fills
from ..utils import EventHandler
from .fundamental_component import KeyboardFocusableFundamentalComponent
from .keyboard_focusable_components import KeyboardFocusableFundamentalComponent
__all__ = ["MediaPlayer"]

View File

@@ -8,7 +8,7 @@ from uniserde import JsonDoc
import rio
from .fundamental_component import KeyboardFocusableFundamentalComponent
from .keyboard_focusable_components import KeyboardFocusableFundamentalComponent
__all__ = [
"MultiLineTextInput",

View File

@@ -7,7 +7,7 @@ import imy.docstrings
import rio
from .component import Component
from .keyboard_focusable_components import KeyboardFocusableComponent
__all__ = [
"NumberInput",
@@ -83,7 +83,7 @@ class NumberInputFocusEvent:
@t.final
class NumberInput(Component):
class NumberInput(KeyboardFocusableComponent):
"""
Like `TextInput`, but specifically for inputting numbers.
@@ -217,8 +217,21 @@ class NumberInput(Component):
on_gain_focus: rio.EventHandler[NumberInputFocusEvent] = None
on_lose_focus: rio.EventHandler[NumberInputFocusEvent] = None
def __post_init__(self) -> None:
self._text_input = None
def __post_init__(self):
self._text_input = rio.TextInput(
text=self._formatted_value(),
label=self.label,
style=self.style,
prefix_text=self.prefix_text,
suffix_text=self.suffix_text,
is_sensitive=self.is_sensitive,
is_valid=self.is_valid,
accessibility_label=self.accessibility_label,
auto_focus=self.auto_focus,
on_confirm=self._on_confirm,
on_gain_focus=self._on_gain_focus,
on_lose_focus=self._on_lose_focus,
)
def _try_set_value(self, raw_value: str) -> bool:
"""
@@ -348,27 +361,7 @@ class NumberInput(Component):
return f"{integer_part_with_sep}{self.session._decimal_separator}{frac_str}"
def build(self) -> rio.Component:
# Build the component
self._text_input = rio.TextInput(
text=self._formatted_value(),
label=self.label,
style=self.style,
prefix_text=self.prefix_text,
suffix_text=self.suffix_text,
is_sensitive=self.is_sensitive,
is_valid=self.is_valid,
accessibility_label=self.accessibility_label,
on_confirm=self._on_confirm,
on_gain_focus=self._on_gain_focus,
on_lose_focus=self._on_lose_focus,
)
return self._text_input
async def grab_keyboard_focus(self) -> None:
"""
## Metadata
`public`: False
"""
if self._text_input is not None:
await self._text_input.grab_keyboard_focus()
await self._text_input.grab_keyboard_focus()

View File

@@ -59,6 +59,9 @@ class SwitcherBar(FundamentalComponent, t.Generic[T]):
`names`: The list of names to display for each value. If `None`, the
string representation of each value is used.
`icons`: Optionally, a list of icons that should be displayed next to the
names.
`color`: The color of the switcher bar.
`orientation`: The orientation of the switcher bar.
@@ -170,11 +173,6 @@ class SwitcherBar(FundamentalComponent, t.Generic[T]):
# SCROLLING-REWORK scroll_x: t.Literal["never", "auto", "always"] = "never",
# SCROLLING-REWORK scroll_y: t.Literal["never", "auto", "always"] = "never",
):
"""
## Parameters
`icons`: The list of icons to display along with with each name.
"""
super().__init__(
key=key,
margin=margin,

60
rio/components/tabs.py Normal file
View File

@@ -0,0 +1,60 @@
import typing as t
import typing_extensions as te
from .component import Component
class Tab(t.TypedDict):
name: str
icon: te.NotRequired[str]
content: Component
class Tabs(Component):
tabs: tuple[Tab, ...]
def __init__(
self,
*tabs: Tab,
key: str | int | None = None,
margin: float | None = None,
margin_x: float | None = None,
margin_y: float | None = None,
margin_left: float | None = None,
margin_top: float | None = None,
margin_right: float | None = None,
margin_bottom: float | None = None,
min_width: float = 0,
min_height: float = 0,
# MAX-SIZE-BRANCH max_width: float | None = None,
# MAX-SIZE-BRANCH max_height: float | None = None,
grow_x: bool = False,
grow_y: bool = False,
align_x: float | None = None,
align_y: float | None = None,
# SCROLLING-REWORK scroll_x: t.Literal["never", "auto", "always"] = "never",
# SCROLLING-REWORK scroll_y: t.Literal["never", "auto", "always"] = "never",
):
super().__init__(
key=key,
margin=margin,
margin_x=margin_x,
margin_y=margin_y,
margin_left=margin_left,
margin_top=margin_top,
margin_right=margin_right,
margin_bottom=margin_bottom,
min_width=min_width,
min_height=min_height,
# MAX-SIZE-BRANCH max_width=max_width,
# MAX-SIZE-BRANCH max_height=max_height,
grow_x=grow_x,
grow_y=grow_y,
align_x=align_x,
align_y=align_y,
# SCROLLING-REWORK scroll_x=scroll_x,
# SCROLLING-REWORK scroll_y=scroll_y,
)
self.tabs = tabs

View File

@@ -7,7 +7,7 @@ import imy.docstrings
import rio
from .fundamental_component import KeyboardFocusableFundamentalComponent
from .keyboard_focusable_components import KeyboardFocusableFundamentalComponent
__all__ = [
"TextInput",

View File

@@ -258,8 +258,14 @@ class PendingAttributeBinding:
def __getattr__(self, name: str):
operation = f".{name}"
self._warn_about_incorrect_usage(operation)
raise AttributeError(self._get_error_message(operation))
def __getitem__(self, item: object):
operation = f"[{item!r}]"
self._warn_about_incorrect_usage(operation)
raise TypeError(self._get_error_message(operation))
def __add__(self, other):
self._warn_about_incorrect_usage("+")
return NotImplemented