mirror of
https://github.com/rio-labs/rio.git
synced 2026-02-05 21:28:30 -06:00
remove JS layouting
This commit is contained in:
committed by
Jakob Pinterits
parent
6e4975cce0
commit
fe5c5abfa6
@@ -1,6 +1,5 @@
|
||||
import { getComponentByElement } from './componentManagement';
|
||||
import { Debouncer } from './debouncer';
|
||||
import { updateLayout } from './layouting';
|
||||
import {
|
||||
callRemoteMethodDiscardResponse,
|
||||
incomingMessageQueue,
|
||||
@@ -130,19 +129,6 @@ async function main(): Promise<void> {
|
||||
window.innerWidth,
|
||||
window.innerHeight
|
||||
);
|
||||
|
||||
// Re-layout, but only if a root component already exists
|
||||
let rootElement = document.body.querySelector(
|
||||
'.rio-fundamental-root-component'
|
||||
);
|
||||
|
||||
if (rootElement !== null) {
|
||||
let rootInstance = getComponentByElement(
|
||||
rootElement as HTMLElement
|
||||
);
|
||||
rootInstance.makeLayoutDirty();
|
||||
updateLayout();
|
||||
}
|
||||
});
|
||||
|
||||
// Process initial messages
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { AlignComponent } from './components/align';
|
||||
import { BuildFailedComponent } from './components/buildFailed';
|
||||
import { ButtonComponent } from './components/button';
|
||||
import { CalendarComponent } from './components/calendar';
|
||||
@@ -27,7 +26,6 @@ import { KeyEventListenerComponent } from './components/keyEventListener';
|
||||
import { LayoutDisplayComponent } from './components/layoutDisplay';
|
||||
import { LinkComponent } from './components/link';
|
||||
import { ListViewComponent } from './components/listView';
|
||||
import { MarginComponent } from './components/margin';
|
||||
import { MarkdownComponent } from './components/markdown';
|
||||
import { MediaPlayerComponent } from './components/mediaPlayer';
|
||||
import { MouseEventListenerComponent } from './components/mouseEventListener';
|
||||
@@ -43,7 +41,6 @@ import { ProgressCircleComponent } from './components/progressCircle';
|
||||
import { RectangleComponent } from './components/rectangle';
|
||||
import { reprElement, scrollToUrlFragment } from './utils';
|
||||
import { RevealerComponent } from './components/revealer';
|
||||
import { ScrollContainerComponent } from './components/scrollContainer';
|
||||
import { ScrollTargetComponent } from './components/scrollTarget';
|
||||
import { SeparatorComponent } from './components/separator';
|
||||
import { SeparatorListItemComponent } from './components/separatorListItem';
|
||||
@@ -58,10 +55,8 @@ import { TextComponent } from './components/text';
|
||||
import { TextInputComponent } from './components/textInput';
|
||||
import { ThemeContextSwitcherComponent } from './components/themeContextSwitcher';
|
||||
import { TooltipComponent } from './components/tooltip';
|
||||
import { updateLayout } from './layouting';
|
||||
|
||||
const COMPONENT_CLASSES = {
|
||||
'Align-builtin': AlignComponent,
|
||||
'BuildFailed-builtin': BuildFailedComponent,
|
||||
'Button-builtin': ButtonComponent,
|
||||
'Calendar-builtin': CalendarComponent,
|
||||
@@ -88,7 +83,6 @@ const COMPONENT_CLASSES = {
|
||||
'LayoutDisplay-builtin': LayoutDisplayComponent,
|
||||
'Link-builtin': LinkComponent,
|
||||
'ListView-builtin': ListViewComponent,
|
||||
'Margin-builtin': MarginComponent,
|
||||
'Markdown-builtin': MarkdownComponent,
|
||||
'MediaPlayer-builtin': MediaPlayerComponent,
|
||||
'MouseEventListener-builtin': MouseEventListenerComponent,
|
||||
@@ -103,7 +97,6 @@ const COMPONENT_CLASSES = {
|
||||
'Rectangle-builtin': RectangleComponent,
|
||||
'Revealer-builtin': RevealerComponent,
|
||||
'Row-builtin': RowComponent,
|
||||
'ScrollContainer-builtin': ScrollContainerComponent,
|
||||
'ScrollTarget-builtin': ScrollTargetComponent,
|
||||
'Separator-builtin': SeparatorComponent,
|
||||
'SeparatorListItem-builtin': SeparatorListItemComponent,
|
||||
@@ -141,15 +134,6 @@ export function getRootComponent(): FundamentalRootComponent {
|
||||
) as FundamentalRootComponent;
|
||||
}
|
||||
|
||||
export function getRootScroller(): ScrollContainerComponent {
|
||||
let rootComponent = getRootComponent();
|
||||
return componentsById[
|
||||
rootComponent.state.content
|
||||
] as ScrollContainerComponent;
|
||||
}
|
||||
|
||||
globalThis.getRootScroller = getRootScroller; // Used to scroll up after navigating to a different page
|
||||
|
||||
export function getComponentByElement(element: Element): ComponentBase {
|
||||
let instance = tryGetComponentByElement(element);
|
||||
|
||||
@@ -183,14 +167,33 @@ globalThis.getInstanceByElement = getComponentByElement; // For debugging
|
||||
export function tryGetComponentByElement(
|
||||
element: Element
|
||||
): ComponentBase | null {
|
||||
return componentsByElement.get(element as HTMLElement) ?? null;
|
||||
let component = componentsByElement.get(element as HTMLElement);
|
||||
if (component !== undefined) {
|
||||
return component;
|
||||
}
|
||||
|
||||
// Components may create additional HTML elements for layouting purposes
|
||||
// (alignment, scrolling, ...), so check if this is such an element
|
||||
if (element instanceof HTMLElement) {
|
||||
let ownerId = element.dataset.ownerId;
|
||||
|
||||
if (ownerId !== undefined) {
|
||||
component = componentsById[ownerId];
|
||||
|
||||
if (component !== undefined) {
|
||||
return component;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function isComponentElement(element: Element): boolean {
|
||||
return componentsByElement.has(element as HTMLElement);
|
||||
}
|
||||
|
||||
export function getParentComponentElementIncludingInjected(
|
||||
export function getParentComponentElement(
|
||||
element: HTMLElement
|
||||
): HTMLElement | null {
|
||||
let curElement = element.parentElement;
|
||||
@@ -206,84 +209,6 @@ export function getParentComponentElementIncludingInjected(
|
||||
return null;
|
||||
}
|
||||
|
||||
function getCurrentComponentState(
|
||||
id: ComponentId,
|
||||
deltaState: ComponentState
|
||||
): ComponentState {
|
||||
let instance = componentsById[id];
|
||||
|
||||
if (instance === undefined) {
|
||||
return deltaState;
|
||||
}
|
||||
|
||||
return {
|
||||
...instance.state,
|
||||
...deltaState,
|
||||
};
|
||||
}
|
||||
|
||||
function createLayoutComponentStates(
|
||||
componentId: ComponentId,
|
||||
message: { [id: string]: ComponentState }
|
||||
): ComponentId {
|
||||
let deltaState = message[componentId] || {};
|
||||
let entireState = getCurrentComponentState(componentId, deltaState);
|
||||
let resultId = componentId;
|
||||
|
||||
// Margin
|
||||
let margin = entireState['_margin_']!;
|
||||
if (margin === undefined) {
|
||||
console.error(`Got incomplete state for component ${componentId}`);
|
||||
} else if (
|
||||
margin[0] !== 0 ||
|
||||
margin[1] !== 0 ||
|
||||
margin[2] !== 0 ||
|
||||
margin[3] !== 0
|
||||
) {
|
||||
let marginId = (componentId * -10) as ComponentId;
|
||||
message[marginId] = {
|
||||
_type_: 'Margin-builtin',
|
||||
_python_type_: 'Margin (injected)',
|
||||
_key_: null,
|
||||
_margin_: [0, 0, 0, 0],
|
||||
_size_: [0, 0],
|
||||
_grow_: entireState._grow_,
|
||||
_rio_internal_: true,
|
||||
// @ts-ignore
|
||||
content: resultId,
|
||||
margin_left: margin[0],
|
||||
margin_top: margin[1],
|
||||
margin_right: margin[2],
|
||||
margin_bottom: margin[3],
|
||||
};
|
||||
resultId = marginId;
|
||||
}
|
||||
|
||||
// Align
|
||||
let align = entireState['_align_']!;
|
||||
if (align === undefined) {
|
||||
console.error(`Got incomplete state for component ${componentId}`);
|
||||
} else if (align[0] !== null || align[1] !== null) {
|
||||
let alignId = (componentId * -10 - 1) as ComponentId;
|
||||
message[alignId] = {
|
||||
_type_: 'Align-builtin',
|
||||
_python_type_: 'Align (injected)',
|
||||
_key_: null,
|
||||
_margin_: [0, 0, 0, 0],
|
||||
_size_: [0, 0],
|
||||
_grow_: entireState._grow_,
|
||||
_rio_internal_: true,
|
||||
// @ts-ignore
|
||||
content: resultId,
|
||||
align_x: align[0],
|
||||
align_y: align[1],
|
||||
};
|
||||
resultId = alignId;
|
||||
}
|
||||
|
||||
return resultId;
|
||||
}
|
||||
|
||||
/// Given a state, return the ids of all its children
|
||||
export function getChildIds(state: ComponentState): ComponentId[] {
|
||||
let result: ComponentId[] = [];
|
||||
@@ -304,105 +229,10 @@ export function getChildIds(state: ComponentState): ComponentId[] {
|
||||
return result;
|
||||
}
|
||||
|
||||
function replaceChildrenWithLayoutComponents(
|
||||
deltaState: ComponentState,
|
||||
childIds: Set<ComponentId>,
|
||||
message: { [id: string]: ComponentState }
|
||||
): void {
|
||||
let propertyNamesWithChildren =
|
||||
globalThis.CHILD_ATTRIBUTE_NAMES[deltaState['_type_']!] || [];
|
||||
|
||||
function uninjectedId(id: ComponentId): ComponentId {
|
||||
if (id >= 0) {
|
||||
return id;
|
||||
}
|
||||
|
||||
return Math.floor(id / -10) as ComponentId;
|
||||
}
|
||||
|
||||
for (let propertyName of propertyNamesWithChildren) {
|
||||
let propertyValue = deltaState[propertyName] as
|
||||
| ComponentId[]
|
||||
| ComponentId
|
||||
| null
|
||||
| undefined;
|
||||
|
||||
if (Array.isArray(propertyValue)) {
|
||||
deltaState[propertyName] = propertyValue.map(
|
||||
(childId: ComponentId): ComponentId => {
|
||||
childId = uninjectedId(childId);
|
||||
childIds.add(childId);
|
||||
return createLayoutComponentStates(childId, message);
|
||||
}
|
||||
);
|
||||
} else if (propertyValue !== null && propertyValue !== undefined) {
|
||||
let childId = uninjectedId(propertyValue);
|
||||
deltaState[propertyName] = createLayoutComponentStates(
|
||||
childId,
|
||||
message
|
||||
);
|
||||
childIds.add(childId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function preprocessDeltaStates(message: {
|
||||
[id: string]: ComponentState;
|
||||
}): void {
|
||||
// Fortunately the root component is created internally by the server, so we
|
||||
// don't need to worry about it having a margin or alignment.
|
||||
|
||||
let originalComponentIds = Object.keys(message).map((id) =>
|
||||
parseInt(id)
|
||||
) as ComponentId[];
|
||||
|
||||
// Keep track of which components have their parents in the message
|
||||
let childIds: Set<ComponentId> = new Set();
|
||||
|
||||
// Walk over all components in the message and inject layout components. The
|
||||
// message is modified in-place, so take care to have a copy of all keys
|
||||
// (`originalComponentIds`)
|
||||
for (let componentId of originalComponentIds) {
|
||||
replaceChildrenWithLayoutComponents(
|
||||
message[componentId],
|
||||
childIds,
|
||||
message
|
||||
);
|
||||
}
|
||||
|
||||
// Find all components which have had a layout component injected, and make
|
||||
// sure their parents are updated to point to the new component.
|
||||
for (let componentId of originalComponentIds) {
|
||||
// Child of another component in the message
|
||||
if (childIds.has(componentId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// The parent isn't contained in the message. Find and add it.
|
||||
let child = componentsById[componentId];
|
||||
if (child === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let parent = child.getParentExcludingInjected();
|
||||
if (parent === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let newParentState = { ...parent.state };
|
||||
replaceChildrenWithLayoutComponents(newParentState, childIds, message);
|
||||
message[parent.id] = newParentState;
|
||||
}
|
||||
}
|
||||
|
||||
export function updateComponentStates(
|
||||
deltaStates: { [id: string]: ComponentState },
|
||||
rootComponentId: ComponentId | null
|
||||
): void {
|
||||
// Preprocess the message. This converts `_align_` and `_margin_` properties
|
||||
// into actual components, amongst other things.
|
||||
preprocessDeltaStates(deltaStates);
|
||||
|
||||
// Modifying the DOM makes the keyboard focus get lost. Remember which
|
||||
// element had focus so we can restore it later.
|
||||
let focusedElement = document.activeElement;
|
||||
@@ -470,19 +300,6 @@ export function updateComponentStates(
|
||||
// Perform updates specific to this component type
|
||||
component.updateElement(deltaState, latentComponents);
|
||||
|
||||
// If the component's width or height has changed, request a re-layout.
|
||||
let width_changed =
|
||||
Math.abs(deltaState._size_![0] - component.state._size_[0]) > 1e-6;
|
||||
let height_changed =
|
||||
Math.abs(deltaState._size_![1] - component.state._size_[1]) > 1e-6;
|
||||
|
||||
if (width_changed || height_changed) {
|
||||
console.debug(
|
||||
`Triggering re-layout because component #${id} changed size: ${component.state._size_} -> ${deltaState._size_}`
|
||||
);
|
||||
component.makeLayoutDirty();
|
||||
}
|
||||
|
||||
// Update the component's state
|
||||
component.state = {
|
||||
...component.state,
|
||||
@@ -515,9 +332,6 @@ export function updateComponentStates(
|
||||
}
|
||||
}
|
||||
|
||||
// Update the layout
|
||||
updateLayout();
|
||||
|
||||
// If this is the first time, check if there's an #url-fragment and scroll
|
||||
// to it
|
||||
if (rootComponentId !== null) {
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
import { componentsById } from '../componentManagement';
|
||||
import { LayoutContext } from '../layouting';
|
||||
import { ComponentId } from '../dataModels';
|
||||
import { ComponentBase, ComponentState } from './componentBase';
|
||||
|
||||
export type AlignState = ComponentState & {
|
||||
_type_: 'Align-builtin';
|
||||
content?: ComponentId;
|
||||
align_x?: number | null;
|
||||
align_y?: number | null;
|
||||
};
|
||||
|
||||
export class AlignComponent extends ComponentBase {
|
||||
state: Required<AlignState>;
|
||||
|
||||
createElement(): HTMLElement {
|
||||
let element = document.createElement('div');
|
||||
return element;
|
||||
}
|
||||
|
||||
updateElement(
|
||||
deltaState: AlignState,
|
||||
latentComponents: Set<ComponentBase>
|
||||
): void {
|
||||
this.replaceOnlyChild(latentComponents, deltaState.content);
|
||||
|
||||
if (
|
||||
deltaState.align_x !== undefined ||
|
||||
deltaState.align_y !== undefined
|
||||
) {
|
||||
this.makeLayoutDirty();
|
||||
}
|
||||
}
|
||||
|
||||
updateNaturalWidth(ctx: LayoutContext): void {
|
||||
this.naturalWidth = componentsById[this.state.content]!.requestedWidth;
|
||||
}
|
||||
|
||||
updateAllocatedWidth(ctx: LayoutContext): void {
|
||||
let child = componentsById[this.state.content]!;
|
||||
|
||||
if (this.state.align_x === null) {
|
||||
child.allocatedWidth = this.allocatedWidth;
|
||||
child.element.style.left = '0';
|
||||
} else {
|
||||
child.allocatedWidth = child.requestedWidth;
|
||||
|
||||
let additionalSpace = this.allocatedWidth - child.requestedWidth;
|
||||
child.element.style.left =
|
||||
additionalSpace * this.state.align_x + 'rem';
|
||||
}
|
||||
}
|
||||
|
||||
updateNaturalHeight(ctx: LayoutContext): void {
|
||||
this.naturalHeight =
|
||||
componentsById[this.state.content]!.requestedHeight;
|
||||
}
|
||||
|
||||
updateAllocatedHeight(ctx: LayoutContext): void {
|
||||
let child = componentsById[this.state.content]!;
|
||||
|
||||
if (this.state.align_y === null) {
|
||||
child.allocatedHeight = this.allocatedHeight;
|
||||
child.element.style.top = '0';
|
||||
} else {
|
||||
child.allocatedHeight = child.requestedHeight;
|
||||
|
||||
let additionalSpace = this.allocatedHeight - child.requestedHeight;
|
||||
child.element.style.top =
|
||||
additionalSpace * this.state.align_y + 'rem';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,4 @@
|
||||
import { applyIcon } from '../designApplication';
|
||||
import { getElementDimensions } from '../layoutHelpers';
|
||||
import { LayoutContext } from '../layouting';
|
||||
import { ComponentBase, ComponentState } from './componentBase';
|
||||
|
||||
export type BuildFailedState = ComponentState & {
|
||||
@@ -60,6 +58,8 @@ export class BuildFailedComponent extends ComponentBase {
|
||||
deltaState: BuildFailedState,
|
||||
latentComponents: Set<ComponentBase>
|
||||
): void {
|
||||
super.updateElement(deltaState, latentComponents);
|
||||
|
||||
if (deltaState.error_summary !== undefined) {
|
||||
this.summaryElement.innerText = deltaState.error_summary;
|
||||
}
|
||||
@@ -68,33 +68,4 @@ export class BuildFailedComponent extends ComponentBase {
|
||||
this.detailsElement.innerText = deltaState.error_details;
|
||||
}
|
||||
}
|
||||
|
||||
updateNaturalWidth(ctx: LayoutContext): void {
|
||||
this.naturalWidth = 4;
|
||||
}
|
||||
|
||||
updateNaturalHeight(ctx: LayoutContext): void {
|
||||
this.naturalHeight = 4;
|
||||
}
|
||||
|
||||
updateAllocatedHeight(ctx: LayoutContext): void {
|
||||
// Display the contents based on how much space the component has
|
||||
// received
|
||||
let summaryDims = getElementDimensions(this.summaryElement);
|
||||
let detailsDims = getElementDimensions(this.detailsElement);
|
||||
|
||||
let summaryVisible = this.allocatedWidth > summaryDims[0] + 6; // The padding is a guess
|
||||
let detailsVisible =
|
||||
summaryVisible &&
|
||||
this.allocatedWidth > detailsDims[0] + 1 && // The padding is a guess
|
||||
this.allocatedHeight > detailsDims[1] + 6; // The padding is a guess
|
||||
|
||||
// Special case: No contents provided
|
||||
summaryVisible = summaryVisible && this.state.error_summary.length > 0;
|
||||
detailsVisible = detailsVisible && this.state.error_details.length > 0;
|
||||
|
||||
// Show/hide the elements
|
||||
this.summaryElement.style.display = summaryVisible ? '' : 'none';
|
||||
this.detailsElement.style.display = detailsVisible ? '' : 'none';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,6 +61,8 @@ export class ButtonComponent extends SingleContainer {
|
||||
deltaState: ButtonState,
|
||||
latentComponents: Set<ComponentBase>
|
||||
): void {
|
||||
super.updateElement(deltaState, latentComponents);
|
||||
|
||||
// Update the child
|
||||
this.replaceOnlyChild(
|
||||
latentComponents,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { ComponentBase, ComponentState } from './componentBase';
|
||||
import { LayoutContext } from '../layouting';
|
||||
import { applyIcon } from '../designApplication';
|
||||
|
||||
const CALENDAR_WIDTH = 15.7;
|
||||
@@ -126,6 +125,8 @@ export class CalendarComponent extends ComponentBase {
|
||||
deltaState: CalendarState,
|
||||
latentComponents: Set<ComponentBase>
|
||||
): void {
|
||||
super.updateElement(deltaState, latentComponents);
|
||||
|
||||
// Apply latent changes to the state
|
||||
let dateChanged: boolean = false;
|
||||
|
||||
@@ -358,12 +359,4 @@ export class CalendarComponent extends ComponentBase {
|
||||
// Update the grid
|
||||
this.updateGrid();
|
||||
}
|
||||
|
||||
updateNaturalWidth(ctx: LayoutContext): void {
|
||||
this.naturalWidth = CALENDAR_WIDTH;
|
||||
}
|
||||
|
||||
updateNaturalHeight(ctx: LayoutContext): void {
|
||||
this.naturalHeight = CALENDAR_HEIGHT;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +48,8 @@ export class CardComponent extends SingleContainer {
|
||||
deltaState: CardState,
|
||||
latentComponents: Set<ComponentBase>
|
||||
): void {
|
||||
super.updateElement(deltaState, latentComponents);
|
||||
|
||||
// Update the child
|
||||
this.replaceOnlyChild(latentComponents, deltaState.content);
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { applyIcon } from '../designApplication';
|
||||
import { LayoutContext } from '../layouting';
|
||||
import { ComponentBase, ComponentState } from './componentBase';
|
||||
|
||||
export type CheckboxState = ComponentState & {
|
||||
@@ -56,6 +55,8 @@ export class CheckboxComponent extends ComponentBase {
|
||||
deltaState: CheckboxState,
|
||||
latentComponents: Set<ComponentBase>
|
||||
): void {
|
||||
super.updateElement(deltaState, latentComponents);
|
||||
|
||||
if (deltaState.is_on !== undefined) {
|
||||
if (deltaState.is_on) {
|
||||
this.element.classList.add('is-on');
|
||||
@@ -79,12 +80,4 @@ export class CheckboxComponent extends ComponentBase {
|
||||
this.checkboxElement.disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
updateNaturalWidth(ctx: LayoutContext): void {
|
||||
this.naturalWidth = 1.5;
|
||||
}
|
||||
|
||||
updateNaturalHeight(ctx: LayoutContext): void {
|
||||
this.naturalHeight = 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ export class ClassContainerComponent extends SingleContainer {
|
||||
deltaState: ClassContainerState,
|
||||
latentComponents: Set<ComponentBase>
|
||||
): void {
|
||||
super.updateElement(deltaState, latentComponents);
|
||||
|
||||
this.replaceOnlyChild(latentComponents, deltaState.content);
|
||||
|
||||
if (deltaState.classes !== undefined) {
|
||||
|
||||
@@ -7,8 +7,6 @@ import { ComponentBase, ComponentState } from './componentBase';
|
||||
import hljs from 'highlight.js/lib/common';
|
||||
import { Language } from 'highlight.js';
|
||||
|
||||
import { LayoutContext } from '../layouting';
|
||||
import { getElementHeight, getElementWidth } from '../layoutHelpers';
|
||||
import { setClipboard, firstDefined } from '../utils';
|
||||
import { applyIcon } from '../designApplication';
|
||||
|
||||
@@ -142,12 +140,6 @@ export function convertDivToCodeBlock(
|
||||
export class CodeBlockComponent extends ComponentBase {
|
||||
state: Required<CodeBlockState>;
|
||||
|
||||
// Since laying out an entire codeblock may be intensive, this component
|
||||
// does its best not to re-layout unless needed. This is done by setting the
|
||||
// height request lazily, and only if the width has changed. This value here
|
||||
// is the component's allocated width when the height request was last set.
|
||||
private heightRequestAssumesWidth: number;
|
||||
|
||||
createElement(): HTMLElement {
|
||||
const element = document.createElement('div');
|
||||
return element;
|
||||
@@ -157,6 +149,8 @@ export class CodeBlockComponent extends ComponentBase {
|
||||
deltaState: CodeBlockState,
|
||||
latentComponents: Set<ComponentBase>
|
||||
): void {
|
||||
super.updateElement(deltaState, latentComponents);
|
||||
|
||||
// Find the value sto use
|
||||
let code = firstDefined(deltaState.code, this.state.code);
|
||||
|
||||
@@ -174,28 +168,5 @@ export class CodeBlockComponent extends ComponentBase {
|
||||
language,
|
||||
displayControls
|
||||
);
|
||||
|
||||
// Update the width request
|
||||
//
|
||||
// For some reason the element takes up the whole parent's width
|
||||
// without explicitly setting its width
|
||||
this.element.style.width = 'min-content';
|
||||
this.naturalWidth = getElementWidth(this.element);
|
||||
|
||||
// Any previously calculated height request is no longer valid
|
||||
this.heightRequestAssumesWidth = -1;
|
||||
this.makeLayoutDirty();
|
||||
}
|
||||
|
||||
updateNaturalHeight(ctx: LayoutContext): void {
|
||||
// Is the previous height request still value?
|
||||
if (this.heightRequestAssumesWidth === this.allocatedWidth) {
|
||||
return;
|
||||
}
|
||||
|
||||
// No, re-layout
|
||||
this.element.style.height = 'min-content';
|
||||
this.naturalHeight = getElementHeight(this.element);
|
||||
this.heightRequestAssumesWidth = this.allocatedWidth;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,9 @@
|
||||
import hljs from 'highlight.js/lib/common';
|
||||
import { componentsByElement, componentsById } from '../componentManagement';
|
||||
import { getElementDimensions } from '../layoutHelpers';
|
||||
import { LayoutContext } from '../layouting';
|
||||
import { ComponentId } from '../dataModels';
|
||||
import { ComponentBase, ComponentState } from './componentBase';
|
||||
import { applyIcon } from '../designApplication';
|
||||
|
||||
// Layouting variables needed by both JS and CSS
|
||||
const MAIN_GAP = 1;
|
||||
const BOX_PADDING = 1;
|
||||
const ARROW_SIZE = 3;
|
||||
const ADDITIONAL_SPACE = (BOX_PADDING * 2 + MAIN_GAP) * 2 + ARROW_SIZE;
|
||||
|
||||
export type CodeExplorerState = ComponentState & {
|
||||
_type_: 'CodeExplorer-builtin';
|
||||
source_code?: string;
|
||||
@@ -30,8 +22,6 @@ export class CodeExplorerComponent extends ComponentBase {
|
||||
private sourceHighlighterElement: HTMLElement;
|
||||
private resultHighlighterElement: HTMLElement;
|
||||
|
||||
private sourceCodeDimensions: [number, number];
|
||||
|
||||
createElement(): HTMLElement {
|
||||
// Build the HTML
|
||||
let element = document.createElement('div');
|
||||
@@ -57,16 +47,6 @@ export class CodeExplorerComponent extends ComponentBase {
|
||||
[this.sourceCodeElement, this.arrowElement, this.buildResultElement] =
|
||||
Array.from(element.children) as HTMLElement[];
|
||||
|
||||
// Finish initialization
|
||||
this.sourceCodeElement.style.padding = `${BOX_PADDING}rem`;
|
||||
|
||||
element.style.gap = `${MAIN_GAP}rem`;
|
||||
|
||||
this.arrowElement.style.width = `${ARROW_SIZE}rem`;
|
||||
this.arrowElement.style.height = `${ARROW_SIZE}rem`;
|
||||
|
||||
// this.arrowElement.style.opacity = '0.3';
|
||||
|
||||
// Listen for mouse events
|
||||
this.buildResultElement.addEventListener(
|
||||
'mousemove',
|
||||
@@ -85,6 +65,8 @@ export class CodeExplorerComponent extends ComponentBase {
|
||||
deltaState: CodeExplorerState,
|
||||
latentComponents: Set<ComponentBase>
|
||||
): void {
|
||||
super.updateElement(deltaState, latentComponents);
|
||||
|
||||
// Update the source
|
||||
if (deltaState.source_code !== undefined) {
|
||||
let hlResult = hljs.highlight(deltaState.source_code, {
|
||||
@@ -93,11 +75,6 @@ export class CodeExplorerComponent extends ComponentBase {
|
||||
});
|
||||
this.sourceCodeElement.innerHTML = hlResult.value;
|
||||
|
||||
// Remember the dimensions now, for faster layouting
|
||||
this.sourceCodeDimensions = getElementDimensions(
|
||||
this.sourceCodeElement
|
||||
);
|
||||
|
||||
// Connect event handlers
|
||||
this._connectHighlightEventListeners();
|
||||
|
||||
@@ -390,50 +367,4 @@ export class CodeExplorerComponent extends ComponentBase {
|
||||
// Exhausted all children
|
||||
return null;
|
||||
}
|
||||
|
||||
updateNaturalWidth(ctx: LayoutContext): void {
|
||||
let buildResultElement = componentsById[this.state.build_result]!;
|
||||
|
||||
if (this.state.style === 'horizontal') {
|
||||
this.naturalWidth =
|
||||
this.sourceCodeDimensions[0] +
|
||||
ADDITIONAL_SPACE +
|
||||
buildResultElement.requestedWidth;
|
||||
} else {
|
||||
this.naturalWidth = Math.max(
|
||||
this.sourceCodeDimensions[0],
|
||||
ADDITIONAL_SPACE,
|
||||
buildResultElement.requestedWidth
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
updateAllocatedWidth(ctx: LayoutContext): void {
|
||||
let buildResultElement = componentsById[this.state.build_result]!;
|
||||
buildResultElement.allocatedWidth = buildResultElement.requestedWidth;
|
||||
}
|
||||
|
||||
updateNaturalHeight(ctx: LayoutContext): void {
|
||||
let buildResultElement = componentsById[this.state.build_result]!;
|
||||
|
||||
if (this.state.style === 'horizontal') {
|
||||
this.naturalHeight = Math.max(
|
||||
this.sourceCodeDimensions[1],
|
||||
ADDITIONAL_SPACE,
|
||||
buildResultElement.requestedHeight
|
||||
);
|
||||
} else {
|
||||
this.naturalHeight =
|
||||
this.sourceCodeDimensions[1] +
|
||||
ADDITIONAL_SPACE +
|
||||
buildResultElement.requestedHeight;
|
||||
}
|
||||
}
|
||||
|
||||
updateAllocatedHeight(ctx: LayoutContext): void {
|
||||
let buildResultElement = componentsById[this.state.build_result]!;
|
||||
buildResultElement.allocatedHeight = buildResultElement.requestedHeight;
|
||||
|
||||
// Positioning the child is already done in `updateElement`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { Color } from '../dataModels';
|
||||
import { ComponentBase, ComponentState } from './componentBase';
|
||||
import { hsvToRgb, rgbToHsv, rgbToHex, rgbaToHex } from '../colorConversion';
|
||||
import { LayoutContext } from '../layouting';
|
||||
import { getElementDimensions } from '../layoutHelpers';
|
||||
|
||||
export type ColorPickerState = ComponentState & {
|
||||
_type_: 'ColorPicker-builtin';
|
||||
@@ -17,11 +15,9 @@ export class ColorPickerComponent extends ComponentBase {
|
||||
private squareKnob: HTMLElement;
|
||||
|
||||
private hueBarOuter: HTMLElement;
|
||||
private hueBarInner: HTMLElement;
|
||||
private hueIndicator: HTMLElement;
|
||||
|
||||
private opacityBarOuter: HTMLElement;
|
||||
private opacityBarInner: HTMLElement;
|
||||
private opacityIndicator: HTMLElement;
|
||||
|
||||
private selectedColorLabel: HTMLInputElement;
|
||||
@@ -72,9 +68,6 @@ export class ColorPickerComponent extends ComponentBase {
|
||||
this.hueBarOuter = containerElement.querySelector(
|
||||
'.rio-color-picker-hue-bar'
|
||||
)!;
|
||||
this.hueBarInner = this.hueBarOuter.querySelector(
|
||||
'.rio-color-slider-inner'
|
||||
)!;
|
||||
this.hueIndicator = this.hueBarOuter.querySelector(
|
||||
'.rio-color-picker-knob'
|
||||
)!;
|
||||
@@ -82,9 +75,6 @@ export class ColorPickerComponent extends ComponentBase {
|
||||
this.opacityBarOuter = containerElement.querySelector(
|
||||
'.rio-color-picker-opacity-bar'
|
||||
)!;
|
||||
this.opacityBarInner = this.opacityBarOuter.querySelector(
|
||||
'.rio-color-slider-inner'
|
||||
)!;
|
||||
this.opacityIndicator = this.opacityBarOuter.querySelector(
|
||||
'.rio-color-picker-knob'
|
||||
)!;
|
||||
@@ -122,6 +112,8 @@ export class ColorPickerComponent extends ComponentBase {
|
||||
deltaState: ColorPickerState,
|
||||
latentComponents: Set<ComponentBase>
|
||||
): void {
|
||||
super.updateElement(deltaState, latentComponents);
|
||||
|
||||
// Color
|
||||
//
|
||||
// Many combination of HSV values correspond to the same RGB color.
|
||||
@@ -420,19 +412,4 @@ export class ColorPickerComponent extends ComponentBase {
|
||||
color: this.state.color,
|
||||
});
|
||||
}
|
||||
|
||||
updateNaturalWidth(ctx: LayoutContext): void {
|
||||
this.naturalWidth = 12;
|
||||
}
|
||||
|
||||
updateAllocatedWidth(ctx: LayoutContext): void {}
|
||||
|
||||
updateNaturalHeight(ctx: LayoutContext): void {
|
||||
this.naturalHeight = this.state.pick_opacity ? 16 : 12;
|
||||
}
|
||||
|
||||
updateAllocatedHeight(ctx: LayoutContext): void {
|
||||
// The knobs are positioned based on the component's size. Update them.
|
||||
this.matchComponentToSelectedHsv();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { componentsById, getComponentByElement } from '../componentManagement';
|
||||
import { LayoutContext } from '../layouting';
|
||||
import { callRemoteMethodDiscardResponse } from '../rpc';
|
||||
import {
|
||||
EventHandler,
|
||||
@@ -9,7 +8,8 @@ import {
|
||||
ClickHandler,
|
||||
} from '../eventHandling';
|
||||
import { DevToolsConnectorComponent } from './devToolsConnector';
|
||||
import { ComponentId } from '../dataModels';
|
||||
import { ComponentId, RioScrollBehavior } from '../dataModels';
|
||||
import { insertWrapperElement, replaceElement } from '../utils';
|
||||
|
||||
/// Base for all component states. Updates received from the backend are
|
||||
/// partial, hence most properties may be undefined.
|
||||
@@ -28,6 +28,8 @@ export type ComponentState = {
|
||||
_size_?: [number, number];
|
||||
// Alignment of the component within its parent, if any
|
||||
_align_?: [number | null, number | null];
|
||||
// Scrolling behavior
|
||||
_scroll_?: [RioScrollBehavior, RioScrollBehavior];
|
||||
// Whether the component would like to receive additional space if there is
|
||||
// any left over
|
||||
_grow_?: [boolean, boolean];
|
||||
@@ -53,19 +55,15 @@ export abstract class ComponentBase {
|
||||
parent: ComponentBase | null = null;
|
||||
children = new Set<ComponentBase>();
|
||||
|
||||
isLayoutDirty: boolean;
|
||||
|
||||
naturalWidth: number = 0;
|
||||
naturalHeight: number = 0;
|
||||
|
||||
requestedWidth: number;
|
||||
requestedHeight: number;
|
||||
|
||||
allocatedWidth: number = 0;
|
||||
allocatedHeight: number = 0;
|
||||
|
||||
_eventHandlers = new Set<EventHandler>();
|
||||
|
||||
// Alignment requires an extra HTML element, which will be created on
|
||||
// demand. So when a component is moved around in the DOM, make sure to use
|
||||
// `outerElement` instead of `element`.
|
||||
outerElement: HTMLElement;
|
||||
private outerAlignElement: HTMLElement | null = null;
|
||||
private innerAlignElement: HTMLElement | null = null;
|
||||
|
||||
constructor(id: ComponentId, state: Required<ComponentState>) {
|
||||
this.id = id;
|
||||
this.state = state;
|
||||
@@ -73,25 +71,87 @@ export abstract class ComponentBase {
|
||||
this.element = this.createElement();
|
||||
this.element.classList.add('rio-component');
|
||||
|
||||
this.isLayoutDirty = true;
|
||||
this.outerElement = this.element;
|
||||
}
|
||||
|
||||
/// Mark this element's layout as dirty, and chain up to the parent.
|
||||
makeLayoutDirty(): void {
|
||||
let cur: ComponentBase | null = this;
|
||||
/// Given a partial state update, this function updates the component's HTML
|
||||
/// element to reflect the new state.
|
||||
///
|
||||
/// 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,
|
||||
latentComponents: Set<ComponentBase>
|
||||
): void {
|
||||
if (deltaState._margin_ !== undefined) {
|
||||
this.element.style.marginLeft = `${deltaState._margin_[0]}rem`;
|
||||
this.element.style.marginTop = `${deltaState._margin_[1]}rem`;
|
||||
this.element.style.marginRight = `${deltaState._margin_[2]}rem`;
|
||||
this.element.style.marginBottom = `${deltaState._margin_[3]}rem`;
|
||||
}
|
||||
|
||||
while (cur !== null && !cur.isLayoutDirty) {
|
||||
cur.isLayoutDirty = true;
|
||||
cur = cur.parent;
|
||||
if (deltaState._align_ !== undefined) {
|
||||
this._updateAlign(deltaState._align_);
|
||||
}
|
||||
|
||||
if (deltaState._scroll_ !== undefined) {
|
||||
}
|
||||
}
|
||||
|
||||
isInjectedLayoutComponent(): boolean {
|
||||
// Injected layout components have negative ids
|
||||
return this.id < 0;
|
||||
private _updateAlign(align: [number | null, number | null]): void {
|
||||
if (align[0] === null && align[1] === null) {
|
||||
// Remove the alignElement if we have one
|
||||
if (this.outerAlignElement !== null) {
|
||||
replaceElement(this.outerAlignElement, this.element);
|
||||
this.outerAlignElement.remove();
|
||||
this.outerAlignElement = null;
|
||||
}
|
||||
} else {
|
||||
// Create the alignElement if we don't have one already
|
||||
if (this.outerAlignElement === null) {
|
||||
this.innerAlignElement = insertWrapperElement(this.element);
|
||||
this.outerAlignElement = insertWrapperElement(
|
||||
this.innerAlignElement
|
||||
);
|
||||
|
||||
this.innerAlignElement.classList.add('rio-align-inner');
|
||||
this.outerAlignElement.classList.add('rio-align-outer');
|
||||
|
||||
this.innerAlignElement.dataset.ownerId = `${this.id}`;
|
||||
this.outerAlignElement.dataset.ownerId = `${this.id}`;
|
||||
|
||||
this.outerElement = this.outerAlignElement;
|
||||
}
|
||||
|
||||
let transform = '';
|
||||
|
||||
if (align[0] === null) {
|
||||
this.innerAlignElement!.style.removeProperty('left');
|
||||
this.innerAlignElement!.style.width = '100%';
|
||||
this.innerAlignElement!.classList.add('stretch-child-x');
|
||||
} else {
|
||||
this.innerAlignElement!.style.left = `${align[0] * 100}%`;
|
||||
this.innerAlignElement!.style.width = 'min-content';
|
||||
this.innerAlignElement!.classList.remove('stretch-child-x');
|
||||
transform += `translateX(-${align[0] * 100}%) `;
|
||||
}
|
||||
|
||||
if (align[1] === null) {
|
||||
this.innerAlignElement!.style.removeProperty('top');
|
||||
this.innerAlignElement!.style.height = '100%';
|
||||
this.innerAlignElement!.classList.add('stretch-child-y');
|
||||
} else {
|
||||
this.innerAlignElement!.style.top = `${align[1] * 100}%`;
|
||||
this.innerAlignElement!.style.height = 'min-content';
|
||||
this.innerAlignElement!.classList.remove('stretch-child-y');
|
||||
transform += `translateY(-${align[1] * 100}%) `;
|
||||
}
|
||||
|
||||
this.innerAlignElement!.style.transform = transform;
|
||||
}
|
||||
}
|
||||
|
||||
getParentExcludingInjected(): ComponentBase | null {
|
||||
getParent(): ComponentBase | null {
|
||||
let parent: ComponentBase | null = this.parent;
|
||||
|
||||
while (true) {
|
||||
@@ -99,10 +159,6 @@ export abstract class ComponentBase {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!parent.isInjectedLayoutComponent()) {
|
||||
return parent;
|
||||
}
|
||||
|
||||
parent = parent.parent;
|
||||
}
|
||||
}
|
||||
@@ -144,11 +200,9 @@ export abstract class ComponentBase {
|
||||
|
||||
// Add the child
|
||||
let child = componentsById[childId]!;
|
||||
parentElement.appendChild(child.element);
|
||||
parentElement.appendChild(child.outerElement);
|
||||
|
||||
this.registerChild(latentComponents, child);
|
||||
|
||||
this.makeLayoutDirty();
|
||||
}
|
||||
|
||||
/// Replaces the child of the given HTML element with the given child. The
|
||||
@@ -173,8 +227,6 @@ export abstract class ComponentBase {
|
||||
|
||||
currentChildElement.remove();
|
||||
child.unparent(latentComponents);
|
||||
|
||||
this.makeLayoutDirty();
|
||||
}
|
||||
|
||||
console.assert(parentElement.firstElementChild === null);
|
||||
@@ -201,8 +253,6 @@ export abstract class ComponentBase {
|
||||
parentElement.appendChild(child.element);
|
||||
|
||||
this.registerChild(latentComponents, child);
|
||||
|
||||
this.makeLayoutDirty();
|
||||
}
|
||||
|
||||
/// Replaces all children of the given HTML element with the given children.
|
||||
@@ -221,8 +271,6 @@ export abstract class ComponentBase {
|
||||
return;
|
||||
}
|
||||
|
||||
let dirty = false;
|
||||
|
||||
let curElement = parentElement.firstElementChild;
|
||||
let children = childIds.map((id) => componentsById[id]!);
|
||||
let curIndex = 0;
|
||||
@@ -235,6 +283,7 @@ export abstract class ComponentBase {
|
||||
if (wrapInDivs) {
|
||||
wrap = (element: HTMLElement) => {
|
||||
let wrapper = document.createElement('div');
|
||||
wrapper.classList.add('rio-child-wrapper');
|
||||
wrapper.appendChild(element);
|
||||
return wrapper;
|
||||
};
|
||||
@@ -252,10 +301,9 @@ export abstract class ComponentBase {
|
||||
while (curIndex < children.length) {
|
||||
let child = children[curIndex];
|
||||
|
||||
parentElement.appendChild(wrap(child.element));
|
||||
parentElement.appendChild(wrap(child.outerElement));
|
||||
this.registerChild(latentComponents, child);
|
||||
|
||||
dirty = true;
|
||||
curIndex++;
|
||||
}
|
||||
break;
|
||||
@@ -274,7 +322,6 @@ export abstract class ComponentBase {
|
||||
child.unparent(latentComponents);
|
||||
}
|
||||
|
||||
dirty = true;
|
||||
curElement = nextElement;
|
||||
}
|
||||
break;
|
||||
@@ -287,7 +334,6 @@ export abstract class ComponentBase {
|
||||
if (childElement === null) {
|
||||
let nextElement = curElement.nextElementSibling;
|
||||
curElement.remove();
|
||||
dirty = true;
|
||||
curElement = nextElement;
|
||||
continue;
|
||||
}
|
||||
@@ -303,15 +349,13 @@ export abstract class ComponentBase {
|
||||
|
||||
// This element is not the correct element, insert the correct one
|
||||
// instead
|
||||
parentElement.insertBefore(wrap(expectedChild.element), curElement);
|
||||
parentElement.insertBefore(
|
||||
wrap(expectedChild.outerElement),
|
||||
curElement
|
||||
);
|
||||
this.registerChild(latentComponents, expectedChild);
|
||||
|
||||
curIndex++;
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
if (dirty) {
|
||||
this.makeLayoutDirty();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -328,16 +372,6 @@ export abstract class ComponentBase {
|
||||
}
|
||||
}
|
||||
|
||||
/// Given a partial state update, this function updates the component's HTML
|
||||
/// element to reflect the new state.
|
||||
///
|
||||
/// The `element` parameter is identical to `this.element`. It's passed as
|
||||
/// an argument because it's more efficient than calling `this.element`.
|
||||
abstract updateElement(
|
||||
deltaState: ComponentState,
|
||||
latentComponents: Set<ComponentBase>
|
||||
): void;
|
||||
|
||||
/// Send a message to the python instance corresponding to this component. The
|
||||
/// message is an arbitrary JSON object and will be passed to the instance's
|
||||
/// `_on_message` method.
|
||||
@@ -388,12 +422,6 @@ export abstract class ComponentBase {
|
||||
return new DragHandler(this, args);
|
||||
}
|
||||
|
||||
updateNaturalWidth(ctx: LayoutContext): void {}
|
||||
updateNaturalHeight(ctx: LayoutContext): void {}
|
||||
|
||||
updateAllocatedWidth(ctx: LayoutContext): void {}
|
||||
updateAllocatedHeight(ctx: LayoutContext): void {}
|
||||
|
||||
toString(): string {
|
||||
let class_name = this.constructor.name;
|
||||
return `<${class_name} id:${this.id}>`;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { componentsById, getRootScroller } from '../componentManagement';
|
||||
import { componentsById } from '../componentManagement';
|
||||
import { applyIcon } from '../designApplication';
|
||||
import { ComponentId } from '../dataModels';
|
||||
import { ComponentBase, ComponentState } from './componentBase';
|
||||
import { DevToolsConnectorComponent } from './devToolsConnector';
|
||||
import { Highlighter } from '../highlighter';
|
||||
@@ -61,7 +60,7 @@ export class ComponentTreeComponent extends ComponentBase {
|
||||
deltaState: ComponentTreeState,
|
||||
latentComponents: Set<ComponentBase>
|
||||
): void {
|
||||
console.debug('TREE', deltaState);
|
||||
super.updateElement(deltaState, latentComponents);
|
||||
|
||||
if (deltaState.component_id !== undefined) {
|
||||
// Highlight the tree item
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { RippleEffect } from '../rippleEffect';
|
||||
import { ComponentBase, ComponentState } from './componentBase';
|
||||
import { componentsById } from '../componentManagement';
|
||||
import { LayoutContext } from '../layouting';
|
||||
import { ComponentId } from '../dataModels';
|
||||
|
||||
const PADDING_X: number = 1.5;
|
||||
@@ -28,6 +27,8 @@ export class CustomListItemComponent extends ComponentBase {
|
||||
deltaState: CustomListItemState,
|
||||
latentComponents: Set<ComponentBase>
|
||||
): void {
|
||||
super.updateElement(deltaState, latentComponents);
|
||||
|
||||
// Update the child
|
||||
this.replaceOnlyChild(latentComponents, deltaState.content);
|
||||
|
||||
@@ -59,29 +60,4 @@ export class CustomListItemComponent extends ComponentBase {
|
||||
type: 'press',
|
||||
});
|
||||
}
|
||||
|
||||
updateNaturalWidth(ctx: LayoutContext): void {
|
||||
this.naturalWidth =
|
||||
componentsById[this.state.content]!.requestedWidth + PADDING_X * 2;
|
||||
}
|
||||
|
||||
updateAllocatedWidth(ctx: LayoutContext): void {
|
||||
componentsById[this.state.content]!.allocatedWidth =
|
||||
this.allocatedWidth - PADDING_X * 2;
|
||||
}
|
||||
|
||||
updateNaturalHeight(ctx: LayoutContext): void {
|
||||
this.naturalHeight =
|
||||
componentsById[this.state.content]!.requestedHeight + PADDING_Y * 2;
|
||||
}
|
||||
|
||||
updateAllocatedHeight(ctx: LayoutContext): void {
|
||||
let child = componentsById[this.state.content]!;
|
||||
child.allocatedHeight = this.allocatedHeight - PADDING_Y * 2;
|
||||
|
||||
// Position the child
|
||||
let element = child.element;
|
||||
element.style.left = `${PADDING_X}rem`;
|
||||
element.style.top = `${PADDING_Y}rem`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { ComponentId } from '../dataModels';
|
||||
import { LayoutContext } from '../layouting';
|
||||
import { ComponentBase, ComponentState } from './componentBase';
|
||||
import { ComponentTreeComponent } from './componentTree';
|
||||
import { LayoutDisplayComponent } from './layoutDisplay';
|
||||
@@ -40,19 +39,6 @@ export class DevToolsConnectorComponent extends ComponentBase {
|
||||
return element;
|
||||
}
|
||||
|
||||
updateElement(
|
||||
deltaState: DevToolsConnectorState,
|
||||
latentComponents: Set<ComponentBase>
|
||||
): void {}
|
||||
|
||||
updateNaturalWidth(ctx: LayoutContext): void {
|
||||
this.naturalWidth = 3;
|
||||
}
|
||||
|
||||
updateNaturalHeight(ctx: LayoutContext): void {
|
||||
this.naturalHeight = 7;
|
||||
}
|
||||
|
||||
/// Called when the state of any component changes. This allows the dev
|
||||
/// tools to update their display.
|
||||
public afterComponentStateChange(deltaStates: {
|
||||
|
||||
@@ -2,7 +2,6 @@ import { pixelsPerRem } from '../app';
|
||||
import { commitCss } from '../utils';
|
||||
import { componentsById } from '../componentManagement';
|
||||
import { ComponentBase, ComponentState } from './componentBase';
|
||||
import { LayoutContext } from '../layouting';
|
||||
import { ColorSet, ComponentId } from '../dataModels';
|
||||
import { applySwitcheroo } from '../designApplication';
|
||||
|
||||
@@ -78,6 +77,8 @@ export class DrawerComponent extends ComponentBase {
|
||||
deltaState: DrawerState,
|
||||
latentComponents: Set<ComponentBase>
|
||||
): void {
|
||||
super.updateElement(deltaState, latentComponents);
|
||||
|
||||
// Update the children
|
||||
this.replaceOnlyChild(
|
||||
latentComponents,
|
||||
@@ -99,8 +100,6 @@ export class DrawerComponent extends ComponentBase {
|
||||
'rio-drawer-bottom'
|
||||
);
|
||||
this.element.classList.add(`rio-drawer-${deltaState.side}`);
|
||||
|
||||
this.makeLayoutDirty();
|
||||
}
|
||||
|
||||
// Colorize
|
||||
@@ -333,50 +332,4 @@ export class DrawerComponent extends ComponentBase {
|
||||
this.closeDrawer();
|
||||
}
|
||||
}
|
||||
|
||||
updateNaturalWidth(ctx: LayoutContext): void {
|
||||
let anchorInst = componentsById[this.state.anchor]!;
|
||||
let contentInst = componentsById[this.state.content]!;
|
||||
|
||||
this.naturalWidth = Math.max(
|
||||
anchorInst.requestedWidth,
|
||||
contentInst.requestedWidth
|
||||
);
|
||||
}
|
||||
|
||||
updateAllocatedWidth(ctx: LayoutContext): void {
|
||||
let anchorInst = componentsById[this.state.anchor]!;
|
||||
let contentInst = componentsById[this.state.content]!;
|
||||
|
||||
anchorInst.allocatedWidth = this.allocatedWidth;
|
||||
|
||||
if (this.state.side === 'left' || this.state.side === 'right') {
|
||||
contentInst.allocatedWidth = contentInst.requestedWidth;
|
||||
} else {
|
||||
contentInst.allocatedWidth = this.allocatedWidth;
|
||||
}
|
||||
}
|
||||
|
||||
updateNaturalHeight(ctx: LayoutContext): void {
|
||||
let anchorInst = componentsById[this.state.anchor]!;
|
||||
let contentInst = componentsById[this.state.content]!;
|
||||
|
||||
this.naturalHeight = Math.max(
|
||||
anchorInst.requestedHeight,
|
||||
contentInst.requestedHeight
|
||||
);
|
||||
}
|
||||
|
||||
updateAllocatedHeight(ctx: LayoutContext): void {
|
||||
let anchorInst = componentsById[this.state.anchor]!;
|
||||
let contentInst = componentsById[this.state.content]!;
|
||||
|
||||
anchorInst.allocatedHeight = this.allocatedHeight;
|
||||
|
||||
if (this.state.side === 'top' || this.state.side === 'bottom') {
|
||||
contentInst.allocatedHeight = contentInst.requestedHeight;
|
||||
} else {
|
||||
contentInst.allocatedHeight = this.allocatedHeight;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,6 @@
|
||||
import { ComponentBase, ComponentState } from './componentBase';
|
||||
import { applyIcon } from '../designApplication';
|
||||
import {
|
||||
updateInputBoxNaturalHeight,
|
||||
updateInputBoxNaturalWidth,
|
||||
} from '../inputBoxTools';
|
||||
import { LayoutContext } from '../layouting';
|
||||
import { pixelsPerRem, scrollBarSize } from '../app';
|
||||
import { getTextDimensions } from '../layoutHelpers';
|
||||
|
||||
// Make sure this is in sync with the CSS
|
||||
const RESERVED_WIDTH_FOR_ARROW = 1.3;
|
||||
const DROPDOWN_LIST_HORIZONTAL_PADDING = 1;
|
||||
import { pixelsPerRem } from '../app';
|
||||
|
||||
export type DropdownState = ComponentState & {
|
||||
_type_: 'Dropdown-builtin';
|
||||
@@ -33,8 +23,6 @@ export class DropdownComponent extends ComponentBase {
|
||||
// The currently highlighted option, if any
|
||||
private highlightedOptionElement: HTMLElement | null = null;
|
||||
|
||||
private longestOptionWidth: number = 0;
|
||||
|
||||
createElement(): HTMLElement {
|
||||
// Create the elements
|
||||
let element = document.createElement('div');
|
||||
@@ -415,7 +403,6 @@ export class DropdownComponent extends ComponentBase {
|
||||
}
|
||||
|
||||
match.classList.add('rio-dropdown-option');
|
||||
match.style.padding = `0.6rem ${DROPDOWN_LIST_HORIZONTAL_PADDING}rem`;
|
||||
this.optionsElement.appendChild(match);
|
||||
|
||||
match.addEventListener('mouseenter', () => {
|
||||
@@ -456,18 +443,13 @@ export class DropdownComponent extends ComponentBase {
|
||||
deltaState: DropdownState,
|
||||
latentComponents: Set<ComponentBase>
|
||||
): void {
|
||||
super.updateElement(deltaState, latentComponents);
|
||||
|
||||
let element = this.element;
|
||||
|
||||
if (deltaState.optionNames !== undefined) {
|
||||
this.state.optionNames = deltaState.optionNames;
|
||||
|
||||
this.longestOptionWidth = Math.max(
|
||||
0,
|
||||
...this.state.optionNames.map(
|
||||
(option) => getTextDimensions(option, 'text')[0]
|
||||
)
|
||||
);
|
||||
|
||||
if (this.isOpen) {
|
||||
this._updateOptionEntries();
|
||||
}
|
||||
@@ -478,9 +460,6 @@ export class DropdownComponent extends ComponentBase {
|
||||
'.rio-input-box-label'
|
||||
) as HTMLElement;
|
||||
labelElement.textContent = deltaState.label;
|
||||
|
||||
// Update the layout
|
||||
updateInputBoxNaturalHeight(this, deltaState.label, 0);
|
||||
}
|
||||
|
||||
if (deltaState.selectedName !== undefined) {
|
||||
@@ -510,23 +489,4 @@ export class DropdownComponent extends ComponentBase {
|
||||
this.element.style.removeProperty('--rio-local-text-color');
|
||||
}
|
||||
}
|
||||
|
||||
updateNaturalWidth(ctx: LayoutContext): void {
|
||||
updateInputBoxNaturalWidth(
|
||||
this,
|
||||
this.longestOptionWidth +
|
||||
scrollBarSize +
|
||||
RESERVED_WIDTH_FOR_ARROW +
|
||||
2 * DROPDOWN_LIST_HORIZONTAL_PADDING
|
||||
);
|
||||
}
|
||||
|
||||
updateAllocatedWidth(ctx: LayoutContext): void {
|
||||
this.popupElement.style.width = `${this.allocatedWidth}rem`;
|
||||
}
|
||||
|
||||
updateNaturalHeight(ctx: LayoutContext): void {
|
||||
// This is set during the updateElement() call, so there is nothing to
|
||||
// do here.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { LayoutContext } from '../layouting';
|
||||
import { ComponentId } from '../dataModels';
|
||||
import { ComponentBase, ComponentState } from './componentBase';
|
||||
import { componentsById } from '../componentManagement';
|
||||
@@ -24,23 +23,10 @@ export class FlowComponent extends ComponentBase {
|
||||
deltaState: FlowState,
|
||||
latentComponents: Set<ComponentBase>
|
||||
): void {
|
||||
super.updateElement(deltaState, latentComponents);
|
||||
|
||||
// Update the children
|
||||
this.replaceChildren(latentComponents, deltaState.children);
|
||||
|
||||
// Regardless of whether the children or the spacing changed, a
|
||||
// re-layout is required
|
||||
this.makeLayoutDirty();
|
||||
}
|
||||
|
||||
updateNaturalWidth(ctx: LayoutContext): void {
|
||||
this.naturalWidth = 0;
|
||||
|
||||
for (let child of this.children) {
|
||||
this.naturalWidth = Math.max(
|
||||
this.naturalWidth,
|
||||
child.requestedWidth
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private processRow(row: ComponentBase[], rowWidth: number): void {
|
||||
@@ -92,93 +78,4 @@ export class FlowComponent extends ComponentBase {
|
||||
child.allocatedWidth = child.requestedWidth + spaceToGrow;
|
||||
}
|
||||
}
|
||||
|
||||
updateAllocatedWidth(ctx: LayoutContext): void {
|
||||
// Allow the code below to assume there's at least one child
|
||||
if (this.children.size === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Divide the children into rows
|
||||
//
|
||||
// For performance and simplicity, store the row index and x positions
|
||||
// right inside the children.
|
||||
let posX = 0;
|
||||
let rowIndex = 0;
|
||||
let currentRow: ComponentBase[] = [];
|
||||
|
||||
for (let childId of this.state.children) {
|
||||
let child = componentsById[childId]!;
|
||||
|
||||
// If the child is too wide, move on to the next row
|
||||
if (posX + child.requestedWidth > this.allocatedWidth) {
|
||||
this.processRow(
|
||||
currentRow,
|
||||
Math.max(posX - this.state.column_spacing, 0)
|
||||
);
|
||||
|
||||
posX = 0;
|
||||
++rowIndex;
|
||||
currentRow = [];
|
||||
}
|
||||
|
||||
// Assign the child to the row
|
||||
(child as any)._flowContainer_rowIndex = rowIndex;
|
||||
(child as any)._flowContainer_posX = posX;
|
||||
currentRow.push(child);
|
||||
|
||||
// Advance the position
|
||||
posX += child.requestedWidth + this.state.column_spacing;
|
||||
}
|
||||
|
||||
// Process the final row
|
||||
this.processRow(currentRow, posX - this.state.column_spacing);
|
||||
}
|
||||
|
||||
updateNaturalHeight(ctx: LayoutContext): void {
|
||||
// Allow the code below to assume there's at least one child
|
||||
if (this.children.size === 0) {
|
||||
this.naturalHeight = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the tallest child for each row
|
||||
let rowHeights: number[] = [];
|
||||
|
||||
for (let child of this.children) {
|
||||
let rowIndex = (child as any)._flowContainer_rowIndex;
|
||||
let childHeight = child.requestedHeight;
|
||||
|
||||
if (rowHeights[rowIndex] === undefined) {
|
||||
rowHeights[rowIndex] = childHeight;
|
||||
} else {
|
||||
rowHeights[rowIndex] = Math.max(
|
||||
rowHeights[rowIndex],
|
||||
childHeight
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Determine the total height
|
||||
let rowTops: number[] = [0];
|
||||
|
||||
for (let ii = 0; ii < rowHeights.length; ii++) {
|
||||
rowTops.push(rowTops[ii] + rowHeights[ii] + this.state.row_spacing);
|
||||
}
|
||||
|
||||
this.naturalHeight = rowTops[rowTops.length - 1];
|
||||
|
||||
// Position the children
|
||||
for (let child of this.children) {
|
||||
let rowIndex = (child as any)._flowContainer_rowIndex;
|
||||
let rowTop = rowTops[rowIndex];
|
||||
let childHeight = rowHeights[rowIndex];
|
||||
|
||||
child.element.style.top = `${rowTop}rem`;
|
||||
// child.element.style.height = `${childHeight}rem`;
|
||||
child.allocatedHeight = childHeight;
|
||||
}
|
||||
}
|
||||
|
||||
updateAllocatedHeight(ctx: LayoutContext): void {}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
import { pixelsPerRem } from '../app';
|
||||
import { componentsById } from '../componentManagement';
|
||||
import { LayoutContext } from '../layouting';
|
||||
import { ComponentId } from '../dataModels';
|
||||
import { setConnectionLostPopupVisibleUnlessGoingAway } from '../rpc';
|
||||
import { ComponentBase, ComponentState } from './componentBase';
|
||||
@@ -15,16 +12,10 @@ export type FundamentalRootComponentState = ComponentState & {
|
||||
export class FundamentalRootComponent extends ComponentBase {
|
||||
state: Required<FundamentalRootComponentState>;
|
||||
|
||||
// The width and height for any components that want to span the entire
|
||||
// screen and not scroll. This differs from just the window width/height,
|
||||
// because the dev tools can also take up space and doesn't count as part of
|
||||
// the user's app.
|
||||
public overlayWidth: number = 0;
|
||||
public overlayHeight: number = 0;
|
||||
|
||||
createElement(): HTMLElement {
|
||||
let element = document.createElement('div');
|
||||
element.classList.add('rio-fundamental-root-component');
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
@@ -32,6 +23,8 @@ export class FundamentalRootComponent extends ComponentBase {
|
||||
deltaState: FundamentalRootComponentState,
|
||||
latentComponents: Set<ComponentBase>
|
||||
): void {
|
||||
super.updateElement(deltaState, latentComponents);
|
||||
|
||||
// Update the children
|
||||
let content = deltaState.content ?? this.state.content;
|
||||
let connectionLostComponent =
|
||||
@@ -75,91 +68,5 @@ export class FundamentalRootComponent extends ComponentBase {
|
||||
),
|
||||
0
|
||||
);
|
||||
|
||||
this.makeLayoutDirty();
|
||||
}
|
||||
|
||||
updateNaturalWidth(ctx: LayoutContext): void {
|
||||
// Don't use `window.innerWidth`. It appears to be rounded to the
|
||||
// nearest integer, so it's inaccurate.
|
||||
//
|
||||
// `getBoundingClientRect()` doesn't account for scroll bars, but our
|
||||
// <html> element is set to `overflow: hidden` anyway, so that's not an
|
||||
// issue.
|
||||
let rect = document.documentElement.getBoundingClientRect();
|
||||
this.naturalWidth = this.allocatedWidth = rect.width / pixelsPerRem;
|
||||
this.naturalHeight = this.allocatedHeight = rect.height / pixelsPerRem;
|
||||
}
|
||||
|
||||
updateAllocatedWidth(ctx: LayoutContext): void {
|
||||
// Overlays take up the full window
|
||||
this.overlayWidth = this.allocatedWidth;
|
||||
|
||||
// If the dev tools are visible, account for that
|
||||
if (this.state.dev_tools !== null) {
|
||||
let devToolsComponent = componentsById[this.state.dev_tools]!;
|
||||
devToolsComponent.allocatedWidth = devToolsComponent.requestedWidth;
|
||||
|
||||
// Even if dev tools are provided, only display them if the screen
|
||||
// is wide enough. Having them show up on a tall mobile screen is
|
||||
// very awkward.
|
||||
//
|
||||
// Since the allocated height isn't available here yet, use the
|
||||
// window size instead.
|
||||
let screenWidth = window.innerWidth / pixelsPerRem;
|
||||
let screenHeight = window.innerHeight / pixelsPerRem;
|
||||
|
||||
if (screenWidth > 50 && screenHeight > 30) {
|
||||
devToolsComponent.element.style.removeProperty('display');
|
||||
this.element.classList.remove('rio-dev-tools-hidden');
|
||||
this.overlayWidth -= devToolsComponent.allocatedWidth;
|
||||
} else {
|
||||
devToolsComponent.element.style.display = 'none';
|
||||
this.element.classList.add('rio-dev-tools-hidden');
|
||||
}
|
||||
}
|
||||
|
||||
// The child receives the remaining width. (The child is a
|
||||
// ScrollContainer, it takes care of scrolling if the user content is
|
||||
// too large)
|
||||
let child = componentsById[this.state.content]!;
|
||||
child.allocatedWidth = this.overlayWidth;
|
||||
|
||||
// Despite being an overlay, the connection lost popup should also cover
|
||||
// the dev tools
|
||||
let connectionLostPopup =
|
||||
componentsById[this.state.connection_lost_component]!;
|
||||
connectionLostPopup.allocatedWidth = this.allocatedWidth;
|
||||
}
|
||||
|
||||
updateNaturalHeight(ctx: LayoutContext): void {
|
||||
// Already done in updateNaturalWidth
|
||||
}
|
||||
|
||||
updateAllocatedHeight(ctx: LayoutContext): void {
|
||||
// Overlays take up the full window
|
||||
this.overlayHeight = this.allocatedHeight;
|
||||
|
||||
// If dev tools are present, set their height and position it
|
||||
if (this.state.dev_tools !== null) {
|
||||
let dbgInst = componentsById[this.state.dev_tools]!;
|
||||
dbgInst.allocatedHeight = this.overlayHeight;
|
||||
|
||||
// Position it
|
||||
let dbgElement = dbgInst.element;
|
||||
dbgElement.style.left = `${this.overlayWidth}rem`;
|
||||
dbgElement.style.top = '0';
|
||||
}
|
||||
|
||||
// The connection lost popup is an overlay
|
||||
let connectionLostPopup =
|
||||
componentsById[this.state.connection_lost_component]!;
|
||||
connectionLostPopup.allocatedHeight = this.overlayHeight;
|
||||
|
||||
// The child once again receives the remaining width. (The child is a
|
||||
// ScrollContainer, it takes care of scrolling if the user content is
|
||||
// too large)
|
||||
let child = componentsById[this.state.content]!;
|
||||
child.allocatedHeight = this.overlayHeight;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { componentsById } from '../componentManagement';
|
||||
import { LayoutContext } from '../layouting';
|
||||
import { ComponentId } from '../dataModels';
|
||||
import { range } from '../utils';
|
||||
import { ComponentBase, ComponentState } from './componentBase';
|
||||
@@ -134,6 +133,8 @@ export class GridComponent extends ComponentBase {
|
||||
deltaState: GridState,
|
||||
latentComponents: Set<ComponentBase>
|
||||
): void {
|
||||
super.updateElement(deltaState, latentComponents);
|
||||
|
||||
let element = this.element;
|
||||
|
||||
if (deltaState._children !== undefined) {
|
||||
@@ -148,183 +149,5 @@ export class GridComponent extends ComponentBase {
|
||||
if (deltaState.column_spacing !== undefined) {
|
||||
element.style.columnGap = `${deltaState.column_spacing}rem`;
|
||||
}
|
||||
|
||||
this.makeLayoutDirty();
|
||||
}
|
||||
|
||||
updateNaturalWidth(ctx: LayoutContext): void {
|
||||
this.columnNaturalWidths = new Array(this.nColumns).fill(0);
|
||||
|
||||
// Determine the width of each column
|
||||
for (let gridChild of this.childrenByNumberOfColumns) {
|
||||
let childInstance = componentsById[gridChild.id]!;
|
||||
|
||||
// How much of the child's width can the columns already
|
||||
// accommodate?
|
||||
let availableWidthTotal: number =
|
||||
(gridChild.width - 1) * this.state.column_spacing;
|
||||
let growCols: number[] = [];
|
||||
|
||||
for (let ii of gridChild.allColumns) {
|
||||
let columnWidth = this.columnNaturalWidths[ii];
|
||||
|
||||
availableWidthTotal += columnWidth;
|
||||
if (this.growingColumns.has(ii)) {
|
||||
growCols.push(ii);
|
||||
}
|
||||
}
|
||||
|
||||
let neededSpace =
|
||||
childInstance.requestedWidth - availableWidthTotal;
|
||||
|
||||
// The columns have enough free space
|
||||
if (neededSpace <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Expand the columns
|
||||
let targetColumns =
|
||||
growCols.length > 0 ? growCols : gridChild.allColumns;
|
||||
|
||||
let spacePerColumn = neededSpace / targetColumns.length;
|
||||
|
||||
for (let column of targetColumns) {
|
||||
this.columnNaturalWidths[column] += spacePerColumn;
|
||||
}
|
||||
}
|
||||
|
||||
// Sum over all widths
|
||||
this.naturalWidth = this.state.column_spacing * (this.nColumns - 1);
|
||||
|
||||
for (let columnWidth of this.columnNaturalWidths) {
|
||||
this.naturalWidth += columnWidth;
|
||||
}
|
||||
}
|
||||
|
||||
updateAllocatedWidth(ctx: LayoutContext): void {
|
||||
// Determine the width of each column
|
||||
let additionalSpace = this.allocatedWidth - this.naturalWidth;
|
||||
let targetColumns =
|
||||
this.growingColumns.size > 0
|
||||
? this.growingColumns
|
||||
: new Set(range(0, this.nColumns));
|
||||
|
||||
let spacePerColumn = additionalSpace / targetColumns.size;
|
||||
this.columnAllocatedWidths = [...this.columnNaturalWidths];
|
||||
|
||||
for (let column of targetColumns) {
|
||||
this.columnAllocatedWidths[column] += spacePerColumn;
|
||||
}
|
||||
|
||||
// Pass on the space to the children
|
||||
for (let gridChild of this.childrenByNumberOfColumns) {
|
||||
let childInstance = componentsById[gridChild.id]!;
|
||||
childInstance.allocatedWidth =
|
||||
this.state.column_spacing * (gridChild.width - 1);
|
||||
|
||||
for (let column of gridChild.allColumns) {
|
||||
childInstance.allocatedWidth +=
|
||||
this.columnAllocatedWidths[column];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateNaturalHeight(ctx: LayoutContext): void {
|
||||
this.rowNaturalHeights = new Array(this.nRows).fill(0);
|
||||
|
||||
// Determine the height of each row
|
||||
for (let gridChild of this.childrenByNumberOfRows) {
|
||||
let childInstance = componentsById[gridChild.id]!;
|
||||
|
||||
// How much of the child's height can the rows already accommodate?
|
||||
let availableHeightTotal: number =
|
||||
(gridChild.height - 1) * this.state.row_spacing;
|
||||
let growRows: number[] = [];
|
||||
|
||||
for (let ii of gridChild.allRows) {
|
||||
let rowHeight = this.rowNaturalHeights[ii];
|
||||
|
||||
availableHeightTotal += rowHeight;
|
||||
if (this.growingRows.has(ii)) {
|
||||
growRows.push(ii);
|
||||
}
|
||||
}
|
||||
|
||||
let neededSpace =
|
||||
childInstance.requestedHeight - availableHeightTotal;
|
||||
|
||||
// The rows have enough free space
|
||||
if (neededSpace <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Expand the rows
|
||||
let targetRows = growRows.length > 0 ? growRows : gridChild.allRows;
|
||||
let spacePerRow = neededSpace / targetRows.length;
|
||||
|
||||
for (let row of targetRows) {
|
||||
this.rowNaturalHeights[row] += spacePerRow;
|
||||
}
|
||||
}
|
||||
|
||||
// Sum over all heights
|
||||
this.naturalHeight = this.state.row_spacing * (this.nRows - 1);
|
||||
for (let rowHeight of this.rowNaturalHeights) {
|
||||
this.naturalHeight += rowHeight;
|
||||
}
|
||||
}
|
||||
|
||||
updateAllocatedHeight(ctx: LayoutContext): void {
|
||||
// Determine the height of each row
|
||||
let additionalSpace = this.allocatedHeight - this.naturalHeight;
|
||||
let targetRows =
|
||||
this.growingRows.size > 0
|
||||
? this.growingRows
|
||||
: new Set(range(0, this.nRows));
|
||||
|
||||
let spacePerRow = additionalSpace / targetRows.size;
|
||||
this.rowAllocatedHeights = [...this.rowNaturalHeights];
|
||||
|
||||
for (let row of targetRows) {
|
||||
this.rowAllocatedHeights[row] += spacePerRow;
|
||||
}
|
||||
|
||||
// Precompute position data
|
||||
let columnWidthCumSum: number[] = [0];
|
||||
for (let columnWidth of this.columnAllocatedWidths) {
|
||||
columnWidthCumSum.push(
|
||||
columnWidthCumSum[columnWidthCumSum.length - 1] +
|
||||
columnWidth +
|
||||
this.state.column_spacing
|
||||
);
|
||||
}
|
||||
|
||||
let rowHeightCumSum: number[] = [0];
|
||||
for (let rowHeight of this.rowAllocatedHeights) {
|
||||
rowHeightCumSum.push(
|
||||
rowHeightCumSum[rowHeightCumSum.length - 1] +
|
||||
rowHeight +
|
||||
this.state.row_spacing
|
||||
);
|
||||
}
|
||||
|
||||
// Pass on the space to the children
|
||||
for (let gridChild of this.childrenByNumberOfRows) {
|
||||
let childInstance = componentsById[gridChild.id]!;
|
||||
childInstance.allocatedHeight =
|
||||
this.state.row_spacing * (gridChild.height - 1);
|
||||
|
||||
for (let row of gridChild.allRows) {
|
||||
childInstance.allocatedHeight += this.rowAllocatedHeights[row];
|
||||
}
|
||||
|
||||
// Set everybody's position
|
||||
let childElement = componentsById[gridChild.id]!.element;
|
||||
|
||||
childElement.style.left = `${
|
||||
columnWidthCumSum[gridChild.column]
|
||||
}rem`;
|
||||
childElement.style.top = `${rowHeightCumSum[gridChild.row]}rem`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
import { ComponentBase, ComponentState } from './componentBase';
|
||||
import { textStyleToCss } from '../cssUtils';
|
||||
import { getTextDimensions } from '../layoutHelpers';
|
||||
import { LayoutContext } from '../layouting';
|
||||
|
||||
const PADDING_LEFT: number = 1.0;
|
||||
const PADDING_TOP: number = 1.3;
|
||||
const PADDING_RIGHT: number = 1.0;
|
||||
const PADDING_BOTTOM: number = 0.3;
|
||||
|
||||
export type HeadingListItemState = ComponentState & {
|
||||
_type_: 'HeadingListItem-builtin';
|
||||
@@ -16,9 +9,6 @@ export type HeadingListItemState = ComponentState & {
|
||||
export class HeadingListItemComponent extends ComponentBase {
|
||||
state: Required<HeadingListItemState>;
|
||||
|
||||
private textWidth: number;
|
||||
private textHeight: number;
|
||||
|
||||
createElement(): HTMLElement {
|
||||
// Create the element
|
||||
let element = document.createElement('div');
|
||||
@@ -29,12 +19,6 @@ export class HeadingListItemComponent extends ComponentBase {
|
||||
// duplicate code.
|
||||
Object.assign(element.style, textStyleToCss('heading3'));
|
||||
|
||||
// Apply the padding
|
||||
element.style.paddingLeft = `${PADDING_LEFT}rem`;
|
||||
element.style.paddingTop = `${PADDING_TOP}rem`;
|
||||
element.style.paddingRight = `${PADDING_RIGHT}rem`;
|
||||
element.style.paddingBottom = `${PADDING_BOTTOM}rem`;
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
@@ -42,28 +26,10 @@ export class HeadingListItemComponent extends ComponentBase {
|
||||
deltaState: HeadingListItemState,
|
||||
latentComponents: Set<ComponentBase>
|
||||
): void {
|
||||
super.updateElement(deltaState, latentComponents);
|
||||
|
||||
if (deltaState.text !== undefined) {
|
||||
this.element.textContent = deltaState.text;
|
||||
|
||||
// Cache the text's dimensions
|
||||
[this.textWidth, this.textHeight] = getTextDimensions(
|
||||
deltaState.text,
|
||||
'heading3'
|
||||
);
|
||||
|
||||
this.makeLayoutDirty();
|
||||
}
|
||||
}
|
||||
|
||||
updateNaturalWidth(ctx: LayoutContext): void {
|
||||
this.naturalWidth = PADDING_LEFT + this.textWidth + PADDING_RIGHT;
|
||||
}
|
||||
|
||||
updateAllocatedWidth(ctx: LayoutContext): void {}
|
||||
|
||||
updateNaturalHeight(ctx: LayoutContext): void {
|
||||
this.naturalHeight = PADDING_TOP + this.textHeight + PADDING_BOTTOM;
|
||||
}
|
||||
|
||||
updateAllocatedHeight(ctx: LayoutContext): void {}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
import { getElementDimensions } from '../layoutHelpers';
|
||||
import { LayoutContext } from '../layouting';
|
||||
import { firstDefined } from '../utils';
|
||||
import { ComponentBase, ComponentState } from './componentBase';
|
||||
|
||||
export type HtmlState = ComponentState & {
|
||||
@@ -57,6 +54,8 @@ export class HtmlComponent extends ComponentBase {
|
||||
deltaState: HtmlState,
|
||||
latentComponents: Set<ComponentBase>
|
||||
): void {
|
||||
super.updateElement(deltaState, latentComponents);
|
||||
|
||||
if (deltaState.html !== undefined) {
|
||||
// If the HTML hasn't actually changed from last time, don't do
|
||||
// anything. This is important so scripts don't get re-executed each
|
||||
@@ -71,12 +70,6 @@ export class HtmlComponent extends ComponentBase {
|
||||
|
||||
// Just setting the innerHTML doesn't run scripts. Do that manually.
|
||||
this.runScriptsInElement(this.containerElement);
|
||||
|
||||
// Determine the dimensions of the HTML element
|
||||
[this.naturalWidth, this.naturalHeight] = getElementDimensions(
|
||||
this.containerElement
|
||||
);
|
||||
this.makeLayoutDirty();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
import { ComponentBase, ComponentState } from './componentBase';
|
||||
import { applyFillToSVG, applyIcon } from '../designApplication';
|
||||
import { pixelsPerRem } from '../app';
|
||||
import { LayoutContext } from '../layouting';
|
||||
|
||||
export type IconState = ComponentState & {
|
||||
_type_: 'Icon-builtin';
|
||||
|
||||
@@ -39,6 +39,8 @@ export class ImageComponent extends ComponentBase {
|
||||
deltaState: ImageState,
|
||||
latentComponents: Set<ComponentBase>
|
||||
): void {
|
||||
super.updateElement(deltaState, latentComponents);
|
||||
|
||||
let imgElement = this.imageElement;
|
||||
|
||||
if (
|
||||
|
||||
@@ -706,6 +706,8 @@ export class KeyEventListenerComponent extends SingleContainer {
|
||||
deltaState: KeyEventListenerState,
|
||||
latentComponents: Set<ComponentBase>
|
||||
): void {
|
||||
super.updateElement(deltaState, latentComponents);
|
||||
|
||||
let element = this.element;
|
||||
|
||||
let reportKeyDown = firstDefined(
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { ComponentBase, ComponentState } from './componentBase';
|
||||
import { LayoutContext } from '../layouting';
|
||||
import { componentsById } from '../componentManagement';
|
||||
import { pixelsPerRem } from '../app';
|
||||
import { getDisplayableChildren } from '../devToolsTreeWalk';
|
||||
@@ -73,7 +72,7 @@ export class LayoutDisplayComponent extends ComponentBase {
|
||||
return;
|
||||
}
|
||||
|
||||
let parentComponent = targetComponent.getParentExcludingInjected();
|
||||
let parentComponent = targetComponent.getParent();
|
||||
if (parentComponent === null) {
|
||||
return;
|
||||
}
|
||||
@@ -106,11 +105,10 @@ export class LayoutDisplayComponent extends ComponentBase {
|
||||
deltaState: LayoutDisplayState,
|
||||
latentComponents: Set<ComponentBase>
|
||||
): void {
|
||||
super.updateElement(deltaState, latentComponents);
|
||||
|
||||
// Has the target component changed?
|
||||
if (deltaState.component_id !== undefined) {
|
||||
// Trigger a re-layout
|
||||
this.makeLayoutDirty();
|
||||
|
||||
// Update the content
|
||||
//
|
||||
// This is necessary because the layout update may not trigger a
|
||||
@@ -181,49 +179,6 @@ export class LayoutDisplayComponent extends ComponentBase {
|
||||
this.onChangeLimiter.call();
|
||||
}
|
||||
|
||||
updateNaturalHeight(ctx: LayoutContext): void {
|
||||
// This component doesn't particularly care about its size. However, it
|
||||
// would be nice to have the correct aspect ratio.
|
||||
//
|
||||
// It's probably not remotely legal to access the natural width of
|
||||
// another component during layouting, but what the heck. This doesn't
|
||||
// do anything other than _attempting_ to get the correct aspect ratio.
|
||||
// Without this we're guaranteed to get a wrong one.
|
||||
let targetComponent: ComponentBase =
|
||||
componentsById[this.state.component_id];
|
||||
|
||||
if (targetComponent === undefined) {
|
||||
this.naturalHeight = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
let parentComponent = targetComponent.getParentExcludingInjected();
|
||||
|
||||
if (parentComponent === null || parentComponent.allocatedWidth === 0) {
|
||||
this.naturalHeight = 0;
|
||||
} else {
|
||||
this.naturalHeight =
|
||||
(this.allocatedWidth * parentComponent.allocatedHeight) /
|
||||
parentComponent.allocatedWidth;
|
||||
}
|
||||
|
||||
// With all of that said, never request more than the max requested
|
||||
// height
|
||||
if (this.state.max_requested_height !== null) {
|
||||
this.naturalHeight = Math.min(
|
||||
this.naturalHeight,
|
||||
this.state.max_requested_height
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
updateAllocatedHeight(ctx: LayoutContext): void {
|
||||
// Let the code below assume that we have a reasonable size
|
||||
if (this.allocatedWidth === 0 || this.allocatedHeight === 0) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
updateContent(): void {
|
||||
// Remove any previous content
|
||||
this.parentElement.innerHTML = '';
|
||||
@@ -241,7 +196,7 @@ export class LayoutDisplayComponent extends ComponentBase {
|
||||
}
|
||||
|
||||
// Look up the parent
|
||||
let parentComponent = targetComponent.getParentExcludingInjected();
|
||||
let parentComponent = targetComponent.getParent();
|
||||
let parentLayout: number[];
|
||||
|
||||
if (parentComponent === null) {
|
||||
@@ -433,7 +388,7 @@ export class LayoutDisplayComponent extends ComponentBase {
|
||||
return;
|
||||
}
|
||||
|
||||
let parentComponent = targetComponent.getParentExcludingInjected();
|
||||
let parentComponent = targetComponent.getParent();
|
||||
|
||||
if (parentComponent === null) {
|
||||
this.highlighter.moveTo(null);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { componentsById } from '../componentManagement';
|
||||
import { LayoutContext } from '../layouting';
|
||||
import { ComponentId } from '../dataModels';
|
||||
import { ComponentBase, ComponentState } from './componentBase';
|
||||
|
||||
@@ -10,7 +9,7 @@ export type LinearContainerState = ComponentState & {
|
||||
proportions?: 'homogeneous' | number[] | null;
|
||||
};
|
||||
|
||||
class LinearContainer extends ComponentBase {
|
||||
abstract class LinearContainer extends ComponentBase {
|
||||
state: Required<LinearContainerState>;
|
||||
|
||||
protected nGrowers: number; // Number of children that grow in the major axis
|
||||
@@ -27,19 +26,36 @@ class LinearContainer extends ComponentBase {
|
||||
deltaState: LinearContainerState,
|
||||
latentComponents: Set<ComponentBase>
|
||||
): void {
|
||||
super.updateElement(deltaState, latentComponents);
|
||||
|
||||
// Children
|
||||
if (deltaState.children !== undefined) {
|
||||
this.replaceChildren(
|
||||
latentComponents,
|
||||
deltaState.children,
|
||||
this.element
|
||||
this.element,
|
||||
true
|
||||
);
|
||||
|
||||
// Clear everybody's position
|
||||
for (let childElement of this.element
|
||||
.children as Iterable<HTMLElement>) {
|
||||
childElement.style.left = '0';
|
||||
childElement.style.top = '0';
|
||||
// Set the children's `flex-grow`
|
||||
let hasGrowers = false;
|
||||
for (let [index, childId] of deltaState.children.entries()) {
|
||||
let childComponent = componentsById[childId]!;
|
||||
let childWrapper = this.element.children[index] as HTMLElement;
|
||||
|
||||
if (this.getGrow(childComponent)) {
|
||||
hasGrowers = true;
|
||||
childWrapper.style.flexGrow = '1';
|
||||
} else {
|
||||
childWrapper.style.flexGrow = '0';
|
||||
}
|
||||
}
|
||||
|
||||
// If nobody wants to grow, all of them do
|
||||
if (!hasGrowers) {
|
||||
for (let childWrapper of this.element.children) {
|
||||
(childWrapper as HTMLElement).style.flexGrow = '1';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,118 +70,24 @@ class LinearContainer extends ComponentBase {
|
||||
deltaState.proportions === null
|
||||
) {
|
||||
} else if (deltaState.proportions === 'homogeneous') {
|
||||
throw new Error('not implemented yet');
|
||||
this.totalProportions = this.children.size;
|
||||
} else {
|
||||
throw new Error('not implemented yet');
|
||||
this.totalProportions = deltaState.proportions.reduce(
|
||||
(a, b) => a + b
|
||||
);
|
||||
}
|
||||
|
||||
// Re-layout
|
||||
this.makeLayoutDirty();
|
||||
}
|
||||
|
||||
/// Returns whether the given component grows in the direction of the
|
||||
/// container
|
||||
abstract getGrow(childComponent: ComponentBase): boolean;
|
||||
}
|
||||
|
||||
export class RowComponent extends LinearContainer {
|
||||
updateNaturalWidth(ctx: LayoutContext): void {
|
||||
if (this.state.proportions === null) {
|
||||
this.naturalWidth = 0;
|
||||
this.nGrowers = 0;
|
||||
|
||||
// Add up all children's requested widths
|
||||
for (let child of this.children) {
|
||||
this.naturalWidth += child.requestedWidth;
|
||||
this.nGrowers += child.state._grow_[0] as any as number;
|
||||
}
|
||||
} else {
|
||||
// When proportions are set, growers are ignored. Extra space is
|
||||
// distributed among all children.
|
||||
|
||||
// Each child has a requested width and a proportion number, which
|
||||
// essentially "cuts" the child into a certain number of equally
|
||||
// sized pieces. In order to find our natural width, we need to
|
||||
// determine the width of the largest piece, then multiply that by
|
||||
// the number of total pieces.
|
||||
let proportions =
|
||||
this.state.proportions === 'homogeneous'
|
||||
? Array(this.children.size).fill(1)
|
||||
: this.state.proportions;
|
||||
let maxProportionSize = 0;
|
||||
|
||||
for (let i = 0; i < proportions.length; i++) {
|
||||
let child = componentsById[this.state.children[i]]!;
|
||||
let proportion = proportions[i];
|
||||
|
||||
if (proportion !== 0) {
|
||||
let proportionSize = child.requestedWidth / proportion;
|
||||
|
||||
maxProportionSize = Math.max(
|
||||
maxProportionSize,
|
||||
proportionSize
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.naturalWidth = maxProportionSize * this.totalProportions;
|
||||
}
|
||||
|
||||
// Account for spacing
|
||||
this.naturalWidth +=
|
||||
Math.max(this.children.size - 1, 0) * this.state.spacing;
|
||||
}
|
||||
|
||||
updateAllocatedWidth(ctx: LayoutContext): void {
|
||||
if (this.state.proportions === null) {
|
||||
// If no child wants to grow, we pretend that all of them do.
|
||||
let forceGrow = this.nGrowers === 0;
|
||||
let nGrowers = forceGrow
|
||||
? this.state.children.length
|
||||
: this.nGrowers;
|
||||
|
||||
let additionalSpace = this.allocatedWidth - this.naturalWidth;
|
||||
let additionalSpacePerGrower = additionalSpace / nGrowers;
|
||||
|
||||
for (let child of this.children) {
|
||||
child.allocatedWidth = child.requestedWidth;
|
||||
|
||||
if (child.state._grow_[0] || forceGrow) {
|
||||
child.allocatedWidth += additionalSpacePerGrower;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let proportions =
|
||||
this.state.proportions === 'homogeneous'
|
||||
? Array(this.children.size).fill(1)
|
||||
: this.state.proportions;
|
||||
|
||||
let spacing =
|
||||
Math.max(this.children.size - 1, 0) * this.state.spacing;
|
||||
let proportionSize =
|
||||
(this.allocatedWidth - spacing) / this.totalProportions;
|
||||
|
||||
for (let i = 0; i < proportions.length; i++) {
|
||||
let child = componentsById[this.state.children[i]]!;
|
||||
child.allocatedWidth = proportionSize * proportions[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateNaturalHeight(ctx: LayoutContext): void {
|
||||
this.naturalHeight = 0;
|
||||
|
||||
for (let child of this.children) {
|
||||
this.naturalHeight = Math.max(
|
||||
this.naturalHeight,
|
||||
child.requestedHeight
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
updateAllocatedHeight(ctx: LayoutContext): void {
|
||||
// Assign the allocated height to the children
|
||||
for (let child of this.children) {
|
||||
child.allocatedHeight = this.allocatedHeight;
|
||||
}
|
||||
getGrow(childComponent: ComponentBase): boolean {
|
||||
return childComponent.state._grow_[0];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,105 +98,7 @@ export class ColumnComponent extends LinearContainer {
|
||||
return element;
|
||||
}
|
||||
|
||||
updateNaturalWidth(ctx: LayoutContext): void {
|
||||
this.naturalWidth = 0;
|
||||
|
||||
for (let child of this.children) {
|
||||
this.naturalWidth = Math.max(
|
||||
this.naturalWidth,
|
||||
child.requestedWidth
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
updateAllocatedWidth(ctx: LayoutContext): void {
|
||||
// Assign the allocated width to the children
|
||||
for (let child of this.children) {
|
||||
child.allocatedWidth = this.allocatedWidth;
|
||||
}
|
||||
}
|
||||
|
||||
updateNaturalHeight(ctx: LayoutContext): void {
|
||||
if (this.state.proportions === null) {
|
||||
this.naturalHeight = 0;
|
||||
this.nGrowers = 0;
|
||||
|
||||
// Add up all children's requested heights
|
||||
for (let child of this.children) {
|
||||
this.naturalHeight += child.requestedHeight;
|
||||
this.nGrowers += child.state._grow_[1] as any as number;
|
||||
}
|
||||
} else {
|
||||
// When proportions are set, growers are ignored. Extra space is
|
||||
// distributed among all children.
|
||||
|
||||
// Each child has a requested width and a proportion number, which
|
||||
// essentially "cuts" the child into a certain number of equally
|
||||
// sized pieces. In order to find our natural width, we need to
|
||||
// determine the width of the largest piece, then multiply that by
|
||||
// the number of total pieces.
|
||||
let proportions =
|
||||
this.state.proportions === 'homogeneous'
|
||||
? Array(this.children.size).fill(1)
|
||||
: this.state.proportions;
|
||||
let maxProportionSize = 0;
|
||||
|
||||
for (let i = 0; i < proportions.length; i++) {
|
||||
let child = componentsById[this.state.children[i]]!;
|
||||
let proportion = proportions[i];
|
||||
|
||||
if (proportion !== 0) {
|
||||
let proportionSize = child.requestedHeight / proportion;
|
||||
|
||||
maxProportionSize = Math.max(
|
||||
maxProportionSize,
|
||||
proportionSize
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.naturalHeight = maxProportionSize * this.totalProportions;
|
||||
}
|
||||
|
||||
// Account for spacing
|
||||
this.naturalHeight +=
|
||||
Math.max(this.children.size - 1, 0) * this.state.spacing;
|
||||
}
|
||||
|
||||
updateAllocatedHeight(ctx: LayoutContext): void {
|
||||
// Assign the allocated height to the children
|
||||
if (this.state.proportions === null) {
|
||||
// If no child wants to grow, we pretend that all of them do.
|
||||
let forceGrow = this.nGrowers === 0;
|
||||
let nGrowers = forceGrow
|
||||
? this.state.children.length
|
||||
: this.nGrowers;
|
||||
|
||||
let additionalSpace = this.allocatedHeight - this.naturalHeight;
|
||||
let additionalSpacePerGrower = additionalSpace / nGrowers;
|
||||
|
||||
for (let child of this.children) {
|
||||
child.allocatedHeight = child.requestedHeight;
|
||||
|
||||
if (child.state._grow_[1] || forceGrow) {
|
||||
child.allocatedHeight += additionalSpacePerGrower;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let proportions =
|
||||
this.state.proportions === 'homogeneous'
|
||||
? Array(this.children.size).fill(1)
|
||||
: this.state.proportions;
|
||||
|
||||
let spacing =
|
||||
Math.max(this.children.size - 1, 0) * this.state.spacing;
|
||||
let proportionSize =
|
||||
(this.allocatedHeight - spacing) / this.totalProportions;
|
||||
|
||||
for (let i = 0; i < proportions.length; i++) {
|
||||
let child = componentsById[this.state.children[i]]!;
|
||||
child.allocatedHeight = proportionSize * proportions[i];
|
||||
}
|
||||
}
|
||||
getGrow(childComponent: ComponentBase): boolean {
|
||||
return childComponent.state._grow_[1];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { componentsById } from '../componentManagement';
|
||||
import { getTextDimensions } from '../layoutHelpers';
|
||||
import { LayoutContext } from '../layouting';
|
||||
import { ComponentId } from '../dataModels';
|
||||
import { ComponentBase, ComponentState } from './componentBase';
|
||||
import { hijackLinkElement, navigateToUrl } from '../utils';
|
||||
import { hijackLinkElement } from '../utils';
|
||||
|
||||
export type LinkState = ComponentState & {
|
||||
_type_: 'Link-builtin';
|
||||
@@ -50,6 +48,8 @@ export class LinkComponent extends ComponentBase {
|
||||
deltaState: LinkState,
|
||||
latentComponents: Set<ComponentBase>
|
||||
): void {
|
||||
super.updateElement(deltaState, latentComponents);
|
||||
|
||||
let element = this.element as HTMLAnchorElement;
|
||||
|
||||
// Child Text?
|
||||
@@ -96,43 +96,4 @@ export class LinkComponent extends ComponentBase {
|
||||
element.target = '';
|
||||
}
|
||||
}
|
||||
|
||||
updateNaturalWidth(ctx: LayoutContext): void {
|
||||
if (this.state.child_component === null) {
|
||||
[this.naturalWidth, this.naturalHeight] = getTextDimensions(
|
||||
this.state.child_text!,
|
||||
'text'
|
||||
);
|
||||
} else {
|
||||
this.naturalWidth =
|
||||
componentsById[this.state.child_component]!.requestedWidth;
|
||||
}
|
||||
}
|
||||
|
||||
updateAllocatedWidth(ctx: LayoutContext): void {
|
||||
if (this.state.child_component !== null) {
|
||||
componentsById[this.state.child_component]!.allocatedWidth =
|
||||
this.allocatedWidth;
|
||||
}
|
||||
}
|
||||
|
||||
updateNaturalHeight(ctx: LayoutContext): void {
|
||||
if (this.state.child_component === null) {
|
||||
// Already set in updateRequestedWidth
|
||||
} else {
|
||||
this.naturalHeight =
|
||||
componentsById[this.state.child_component]!.requestedHeight;
|
||||
}
|
||||
}
|
||||
|
||||
updateAllocatedHeight(ctx: LayoutContext): void {
|
||||
if (this.state.child_component !== null) {
|
||||
componentsById[this.state.child_component]!.allocatedHeight =
|
||||
this.allocatedHeight;
|
||||
|
||||
let element = componentsById[this.state.child_component]!.element;
|
||||
element.style.left = '0';
|
||||
element.style.top = '0';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,8 @@ export class ListViewComponent extends ColumnComponent {
|
||||
deltaState: LinearContainerState,
|
||||
latentComponents: Set<ComponentBase>
|
||||
): void {
|
||||
super.updateElement(deltaState, latentComponents);
|
||||
|
||||
// Columns don't wrap their children in divs, but ListView does. Hence
|
||||
// the overridden updateElement.
|
||||
this.replaceChildren(
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
import { ComponentBase, ComponentState } from './componentBase';
|
||||
import { componentsById } from '../componentManagement';
|
||||
import { LayoutContext } from '../layouting';
|
||||
import { ComponentId } from '../dataModels';
|
||||
|
||||
export type MarginState = ComponentState & {
|
||||
_type_: 'Margin-builtin';
|
||||
content?: ComponentId;
|
||||
margin_left?: number;
|
||||
margin_top?: number;
|
||||
margin_right?: number;
|
||||
margin_bottom?: number;
|
||||
};
|
||||
|
||||
export class MarginComponent extends ComponentBase {
|
||||
state: Required<MarginState>;
|
||||
|
||||
createElement(): HTMLElement {
|
||||
let element = document.createElement('div');
|
||||
return element;
|
||||
}
|
||||
|
||||
updateElement(
|
||||
deltaState: MarginState,
|
||||
latentComponents: Set<ComponentBase>
|
||||
): void {
|
||||
this.replaceOnlyChild(latentComponents, deltaState.content);
|
||||
|
||||
if (
|
||||
deltaState.margin_left !== undefined ||
|
||||
deltaState.margin_top !== undefined ||
|
||||
deltaState.margin_right !== undefined ||
|
||||
deltaState.margin_bottom !== undefined
|
||||
) {
|
||||
this.makeLayoutDirty();
|
||||
}
|
||||
}
|
||||
|
||||
updateNaturalWidth(ctx: LayoutContext): void {
|
||||
this.naturalWidth =
|
||||
componentsById[this.state.content]!.requestedWidth +
|
||||
this.state.margin_left +
|
||||
this.state.margin_right;
|
||||
}
|
||||
|
||||
updateAllocatedWidth(ctx: LayoutContext): void {
|
||||
let childInstance = componentsById[this.state.content]!;
|
||||
childInstance.allocatedWidth =
|
||||
this.allocatedWidth -
|
||||
this.state.margin_left -
|
||||
this.state.margin_right;
|
||||
}
|
||||
|
||||
updateNaturalHeight(ctx: LayoutContext): void {
|
||||
this.naturalHeight =
|
||||
componentsById[this.state.content]!.requestedHeight +
|
||||
this.state.margin_top +
|
||||
this.state.margin_bottom;
|
||||
}
|
||||
|
||||
updateAllocatedHeight(ctx: LayoutContext): void {
|
||||
let childInstance = componentsById[this.state.content]!;
|
||||
childInstance.allocatedHeight =
|
||||
this.allocatedHeight -
|
||||
this.state.margin_top -
|
||||
this.state.margin_bottom;
|
||||
|
||||
let childElement = childInstance.element;
|
||||
childElement.style.left = `${this.state.margin_left}rem`;
|
||||
childElement.style.top = `${this.state.margin_top}rem`;
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,6 @@ import { micromark } from 'micromark';
|
||||
// https://github.com/highlightjs/highlight.js#importing-the-library
|
||||
import hljs from 'highlight.js/lib/common';
|
||||
|
||||
import { LayoutContext } from '../layouting';
|
||||
import { getElementHeight, getElementWidth } from '../layoutHelpers';
|
||||
import { firstDefined, hijackLinkElement } from '../utils';
|
||||
import { convertDivToCodeBlock } from './codeBlock';
|
||||
|
||||
@@ -130,6 +128,8 @@ export class MarkdownComponent extends ComponentBase {
|
||||
deltaState: MarkdownState,
|
||||
latentComponents: Set<ComponentBase>
|
||||
): void {
|
||||
super.updateElement(deltaState, latentComponents);
|
||||
|
||||
if (deltaState.text !== undefined) {
|
||||
// Create a new div to hold the markdown content. This is so the
|
||||
// layouting code can move it around as needed.
|
||||
@@ -139,31 +139,6 @@ export class MarkdownComponent extends ComponentBase {
|
||||
);
|
||||
|
||||
convertMarkdown(deltaState.text, this.element, defaultLanguage);
|
||||
|
||||
// Update the width request
|
||||
//
|
||||
// For some reason the element takes up the whole parent's width
|
||||
// without explicitly setting its width
|
||||
this.element.style.width = 'min-content';
|
||||
this.naturalWidth = getElementWidth(this.element);
|
||||
|
||||
// Any previously calculated height request is no longer valid
|
||||
this.heightRequestAssumesWidth = -1;
|
||||
this.makeLayoutDirty();
|
||||
}
|
||||
}
|
||||
|
||||
updateNaturalHeight(ctx: LayoutContext): void {
|
||||
// Is the previous height request still value?
|
||||
if (this.heightRequestAssumesWidth === this.allocatedWidth) {
|
||||
return;
|
||||
}
|
||||
|
||||
// No, re-layout
|
||||
this.element.style.height = 'min-content';
|
||||
this.naturalHeight = getElementHeight(this.element);
|
||||
this.heightRequestAssumesWidth = this.allocatedWidth;
|
||||
}
|
||||
|
||||
updateAllocatedHeight(ctx: LayoutContext): void {}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { fillToCss } from '../cssUtils';
|
||||
import { applyIcon } from '../designApplication';
|
||||
import { LayoutContext } from '../layouting';
|
||||
import { AnyFill } from '../dataModels';
|
||||
import { sleep } from '../utils';
|
||||
import { ComponentBase, ComponentState } from './componentBase';
|
||||
@@ -541,6 +540,8 @@ export class MediaPlayerComponent extends ComponentBase {
|
||||
deltaState: MediaPlayerState,
|
||||
latentComponents: Set<ComponentBase>
|
||||
): void {
|
||||
super.updateElement(deltaState, latentComponents);
|
||||
|
||||
if (deltaState.mediaUrl !== undefined) {
|
||||
let mediaUrl = new URL(deltaState.mediaUrl, document.location.href)
|
||||
.href;
|
||||
@@ -846,12 +847,4 @@ export class MediaPlayerComponent extends ComponentBase {
|
||||
this.mediaPlayer.src = '';
|
||||
this.mediaPlayer.load();
|
||||
}
|
||||
|
||||
updateNaturalWidth(ctx: LayoutContext): void {
|
||||
this.naturalWidth = 16;
|
||||
}
|
||||
|
||||
updateNaturalHeight(ctx: LayoutContext): void {
|
||||
this.naturalHeight = 5;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,6 +70,8 @@ export class MouseEventListenerComponent extends SingleContainer {
|
||||
deltaState: MouseEventListenerState,
|
||||
latentComponents: Set<ComponentBase>
|
||||
): void {
|
||||
super.updateElement(deltaState, latentComponents);
|
||||
|
||||
this.replaceOnlyChild(latentComponents, deltaState.content);
|
||||
|
||||
if (deltaState.reportPress) {
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
import { ComponentBase, ComponentState } from './componentBase';
|
||||
import { LayoutContext } from '../layouting';
|
||||
import {
|
||||
updateInputBoxNaturalHeight,
|
||||
updateInputBoxNaturalWidth,
|
||||
} from '../inputBoxTools';
|
||||
|
||||
export type MultiLineTextInputState = ComponentState & {
|
||||
_type_: 'MultiLineTextInput-builtin';
|
||||
@@ -83,15 +78,14 @@ export class MultiLineTextInputComponent extends ComponentBase {
|
||||
deltaState: MultiLineTextInputState,
|
||||
latentComponents: Set<ComponentBase>
|
||||
): void {
|
||||
super.updateElement(deltaState, latentComponents);
|
||||
|
||||
if (deltaState.text !== undefined) {
|
||||
this.inputElement.value = deltaState.text;
|
||||
}
|
||||
|
||||
if (deltaState.label !== undefined) {
|
||||
this.labelElement.textContent = deltaState.label;
|
||||
|
||||
// Update the layout
|
||||
updateInputBoxNaturalHeight(this, deltaState.label, 0);
|
||||
}
|
||||
|
||||
if (deltaState.is_sensitive === true) {
|
||||
@@ -115,13 +109,4 @@ export class MultiLineTextInputComponent extends ComponentBase {
|
||||
grabKeyboardFocus(): void {
|
||||
this.inputElement.focus();
|
||||
}
|
||||
|
||||
updateNaturalWidth(ctx: LayoutContext): void {
|
||||
updateInputBoxNaturalWidth(this, 0);
|
||||
}
|
||||
|
||||
updateNaturalHeight(ctx: LayoutContext): void {
|
||||
// This is set during the updateElement() call, so there is nothing to
|
||||
// do here.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { ComponentBase, ComponentState } from './componentBase';
|
||||
import { LayoutContext } from '../layouting';
|
||||
import { Color } from '../dataModels';
|
||||
import { getTextDimensions } from '../layoutHelpers';
|
||||
import { colorToCssString } from '../cssUtils';
|
||||
|
||||
export type NodeInputState = ComponentState & {
|
||||
@@ -41,14 +39,11 @@ export class NodeInputComponent extends ComponentBase {
|
||||
deltaState: NodeInputState,
|
||||
latentComponents: Set<ComponentBase>
|
||||
): void {
|
||||
super.updateElement(deltaState, latentComponents);
|
||||
|
||||
// Name
|
||||
if (deltaState.name !== undefined) {
|
||||
this.textElement.textContent = deltaState.name;
|
||||
|
||||
// Cache the dimensions
|
||||
let textDimensions = getTextDimensions(deltaState.name, 'text');
|
||||
this.naturalWidth = textDimensions[0];
|
||||
this.naturalHeight = textDimensions[1];
|
||||
}
|
||||
|
||||
// Color
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { ComponentBase, ComponentState } from './componentBase';
|
||||
import { LayoutContext } from '../layouting';
|
||||
import { Color } from '../dataModels';
|
||||
import { getTextDimensions } from '../layoutHelpers';
|
||||
import { colorToCssString } from '../cssUtils';
|
||||
|
||||
export type NodeOutputState = ComponentState & {
|
||||
@@ -41,14 +39,11 @@ export class NodeOutputComponent extends ComponentBase {
|
||||
deltaState: NodeOutputState,
|
||||
latentComponents: Set<ComponentBase>
|
||||
): void {
|
||||
super.updateElement(deltaState, latentComponents);
|
||||
|
||||
// Name
|
||||
if (deltaState.name !== undefined) {
|
||||
this.textElement.textContent = deltaState.name;
|
||||
|
||||
// Cache the dimensions
|
||||
let textDimensions = getTextDimensions(deltaState.name, 'text');
|
||||
this.naturalWidth = textDimensions[0];
|
||||
this.naturalHeight = textDimensions[1];
|
||||
}
|
||||
|
||||
// Color
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { componentsById, getRootComponent } from '../componentManagement';
|
||||
import { LayoutContext } from '../layouting';
|
||||
import { ComponentId } from '../dataModels';
|
||||
import { ComponentBase, ComponentState } from './componentBase';
|
||||
|
||||
@@ -16,20 +15,6 @@ export class OverlayComponent extends ComponentBase {
|
||||
createElement(): HTMLElement {
|
||||
let element = document.createElement('div');
|
||||
element.classList.add('rio-overlay');
|
||||
|
||||
// When the window is resized, we need re-layouting. This isn't
|
||||
// guaranteed to happen automatically, because if a parent component's
|
||||
// size doesn't change, then its children won't be re-layouted. So we
|
||||
// have to explicitly listen for the resize event and mark ourselves as
|
||||
// dirty.
|
||||
//
|
||||
// The `capture: true` is there to ensure that we're already marked as
|
||||
// dirty when the re-layout is triggered.
|
||||
this.onWindowResize = this.makeLayoutDirty.bind(this);
|
||||
window.addEventListener('resize', this.onWindowResize, {
|
||||
capture: true,
|
||||
});
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
@@ -41,25 +26,8 @@ export class OverlayComponent extends ComponentBase {
|
||||
deltaState: OverlayState,
|
||||
latentComponents: Set<ComponentBase>
|
||||
): void {
|
||||
super.updateElement(deltaState, latentComponents);
|
||||
|
||||
this.replaceOnlyChild(latentComponents, deltaState.content);
|
||||
}
|
||||
|
||||
updateAllocatedWidth(ctx: LayoutContext): void {
|
||||
// The root component keeps track of the correct overlay size. Take it
|
||||
// from there. To heck with what the parent says.
|
||||
let root = getRootComponent();
|
||||
componentsById[this.state.content]!.allocatedWidth = root.overlayWidth;
|
||||
}
|
||||
|
||||
updateAllocatedHeight(ctx: LayoutContext): void {
|
||||
// Honor the global overlay height.
|
||||
let root = getRootComponent();
|
||||
componentsById[this.state.content]!.allocatedHeight =
|
||||
root.overlayHeight;
|
||||
|
||||
// Position the child
|
||||
let element = componentsById[this.state.content]!.element;
|
||||
element.style.left = '0';
|
||||
element.style.top = '0';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,13 +11,17 @@ export class PlaceholderComponent extends SingleContainer {
|
||||
state: Required<PlaceholderState>;
|
||||
|
||||
createElement(): HTMLElement {
|
||||
return document.createElement('div');
|
||||
let element = document.createElement('div');
|
||||
element.classList.add('rio-placeholder');
|
||||
return element;
|
||||
}
|
||||
|
||||
updateElement(
|
||||
deltaState: PlaceholderState,
|
||||
latentComponents: Set<ComponentBase>
|
||||
): void {
|
||||
super.updateElement(deltaState, latentComponents);
|
||||
|
||||
this.replaceOnlyChild(latentComponents, deltaState._child_);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { pixelsPerRem } from '../app';
|
||||
import { fillToCss } from '../cssUtils';
|
||||
import { LayoutContext } from '../layouting';
|
||||
import { AnyFill } from '../dataModels';
|
||||
import { ComponentBase, ComponentState } from './componentBase';
|
||||
|
||||
@@ -62,6 +61,8 @@ export class PlotComponent extends ComponentBase {
|
||||
deltaState: PlotState,
|
||||
latentComponents: Set<ComponentBase>
|
||||
): void {
|
||||
super.updateElement(deltaState, latentComponents);
|
||||
|
||||
if (deltaState.plot !== undefined) {
|
||||
this.element.innerHTML = '';
|
||||
|
||||
@@ -123,14 +124,4 @@ export class PlotComponent extends ComponentBase {
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
updateAllocatedHeight(ctx: LayoutContext): void {
|
||||
// Plotly is too dumb to layout itself. Help out.
|
||||
if (
|
||||
this.state.plot.type === 'plotly' &&
|
||||
window['Plotly'] !== undefined
|
||||
) {
|
||||
this.updatePlotlyLayout();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { pixelsPerRem } from '../app';
|
||||
import { componentsById } from '../componentManagement';
|
||||
import { applySwitcheroo } from '../designApplication';
|
||||
import { LayoutContext } from '../layouting';
|
||||
import { ColorSet, ComponentId } from '../dataModels';
|
||||
import { ComponentBase, ComponentState } from './componentBase';
|
||||
import { PopupManager } from '../popupManager';
|
||||
@@ -55,6 +53,8 @@ export class PopupComponent extends ComponentBase {
|
||||
deltaState: PopupState,
|
||||
latentComponents: Set<ComponentBase>
|
||||
): void {
|
||||
super.updateElement(deltaState, latentComponents);
|
||||
|
||||
// Update the children
|
||||
this.replaceOnlyChild(
|
||||
latentComponents,
|
||||
@@ -105,38 +105,4 @@ export class PopupComponent extends ComponentBase {
|
||||
|
||||
this.popupManager.destroy();
|
||||
}
|
||||
|
||||
updateNaturalWidth(ctx: LayoutContext): void {
|
||||
this.naturalWidth = componentsById[this.state.anchor]!.requestedWidth;
|
||||
}
|
||||
|
||||
updateAllocatedWidth(ctx: LayoutContext): void {
|
||||
let anchorInst = componentsById[this.state.anchor]!;
|
||||
anchorInst.allocatedWidth = this.allocatedWidth;
|
||||
|
||||
let contentInst = componentsById[this.state.content]!;
|
||||
contentInst.allocatedWidth = contentInst.requestedWidth;
|
||||
}
|
||||
|
||||
updateNaturalHeight(ctx: LayoutContext): void {
|
||||
this.naturalHeight = componentsById[this.state.anchor]!.requestedHeight;
|
||||
}
|
||||
|
||||
updateAllocatedHeight(ctx: LayoutContext): void {
|
||||
// Pass on the allocated space
|
||||
let anchorInst = componentsById[this.state.anchor]!;
|
||||
anchorInst.allocatedHeight = this.allocatedHeight;
|
||||
|
||||
let contentInst = componentsById[this.state.content]!;
|
||||
contentInst.allocatedHeight = contentInst.requestedHeight;
|
||||
|
||||
// And position the children
|
||||
let anchorElem = anchorInst.element;
|
||||
anchorElem.style.left = '0';
|
||||
anchorElem.style.top = '0';
|
||||
|
||||
let contentElem = contentInst.element;
|
||||
contentElem.style.left = '0';
|
||||
contentElem.style.top = '0';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { ColorSet } from '../dataModels';
|
||||
import { applySwitcheroo } from '../designApplication';
|
||||
import { LayoutContext } from '../layouting';
|
||||
import { ComponentBase, ComponentState } from './componentBase';
|
||||
|
||||
export type ProgressBarState = ComponentState & {
|
||||
@@ -37,6 +36,8 @@ export class ProgressBarComponent extends ComponentBase {
|
||||
deltaState: ProgressBarState,
|
||||
latentComponents: Set<ComponentBase>
|
||||
): void {
|
||||
super.updateElement(deltaState, latentComponents);
|
||||
|
||||
// No progress specified
|
||||
if (deltaState.progress === undefined) {
|
||||
}
|
||||
@@ -78,12 +79,4 @@ export class ProgressBarComponent extends ComponentBase {
|
||||
this.element.style.removeProperty('border-radius');
|
||||
}
|
||||
}
|
||||
|
||||
updateNaturalWidth(ctx: LayoutContext): void {
|
||||
this.naturalWidth = 3;
|
||||
}
|
||||
|
||||
updateNaturalHeight(ctx: LayoutContext): void {
|
||||
this.naturalHeight = 0.25;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,8 @@ export class ProgressCircleComponent extends ComponentBase {
|
||||
deltaState: ProgressCircleState,
|
||||
latentComponents: Set<ComponentBase>
|
||||
): void {
|
||||
super.updateElement(deltaState, latentComponents);
|
||||
|
||||
// Apply the progress
|
||||
if (deltaState.progress !== undefined) {
|
||||
if (deltaState.progress === null) {
|
||||
|
||||
@@ -67,6 +67,8 @@ export class RectangleComponent extends SingleContainer {
|
||||
deltaState: RectangleState,
|
||||
latentComponents: Set<ComponentBase>
|
||||
): void {
|
||||
super.updateElement(deltaState, latentComponents);
|
||||
|
||||
this.replaceOnlyChild(latentComponents, deltaState.content);
|
||||
|
||||
if (deltaState.transition_time !== undefined) {
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import { componentsById } from '../componentManagement';
|
||||
import { textStyleToCss } from '../cssUtils';
|
||||
import { applyIcon } from '../designApplication';
|
||||
import { easeInOut } from '../easeFunctions';
|
||||
import { getTextDimensions } from '../layoutHelpers';
|
||||
import { LayoutContext, updateLayout } from '../layouting';
|
||||
import { ComponentId, TextStyle } from '../dataModels';
|
||||
import { firstDefined } from '../utils';
|
||||
import { ComponentBase, ComponentState } from './componentBase';
|
||||
@@ -123,6 +119,8 @@ export class RevealerComponent extends ComponentBase {
|
||||
deltaState: RevealerState,
|
||||
latentComponents: Set<ComponentBase>
|
||||
): void {
|
||||
super.updateElement(deltaState, latentComponents);
|
||||
|
||||
// Update the header
|
||||
if (deltaState.header === null) {
|
||||
this.headerElement.style.display = 'none';
|
||||
@@ -189,29 +187,6 @@ export class RevealerComponent extends ComponentBase {
|
||||
this.element.classList.remove('rio-revealer-open');
|
||||
}
|
||||
}
|
||||
|
||||
// Cache the header text's dimensions
|
||||
if (
|
||||
deltaState.header !== undefined ||
|
||||
deltaState.header_style !== undefined
|
||||
) {
|
||||
let headerText = firstDefined(deltaState.header, this.state.header);
|
||||
|
||||
if (headerText !== null) {
|
||||
let headerStyle = firstDefined(
|
||||
deltaState.header_style,
|
||||
this.state.header_style
|
||||
);
|
||||
|
||||
[this.labelWidth, this.labelHeight] = getTextDimensions(
|
||||
headerText,
|
||||
headerStyle
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Re-layout
|
||||
this.makeLayoutDirty();
|
||||
}
|
||||
|
||||
/// If the animation is not yet running, start it. Does nothing otherwise.
|
||||
@@ -244,10 +219,6 @@ export class RevealerComponent extends ComponentBase {
|
||||
Math.min(1, this.openFractionBeforeEase)
|
||||
);
|
||||
|
||||
// Re-layout
|
||||
this.makeLayoutDirty();
|
||||
updateLayout();
|
||||
|
||||
// If the animation is not yet finished, continue it.
|
||||
let target = this.state.is_open ? 1 : 0;
|
||||
if (this.openFractionBeforeEase === target) {
|
||||
@@ -256,67 +227,4 @@ export class RevealerComponent extends ComponentBase {
|
||||
requestAnimationFrame(() => this.animationWorker());
|
||||
}
|
||||
}
|
||||
|
||||
updateNaturalWidth(ctx: LayoutContext): void {
|
||||
// Account for the content
|
||||
this.naturalWidth = componentsById[this.state.content]!.requestedWidth;
|
||||
|
||||
// If a header is present, consider that as well
|
||||
if (this.state.header !== null) {
|
||||
let headerWidth =
|
||||
this.labelWidth + 4 + 2 * HEADER_PADDING * this.headerScale;
|
||||
this.naturalWidth = Math.max(this.naturalWidth, headerWidth);
|
||||
}
|
||||
}
|
||||
|
||||
updateAllocatedWidth(ctx: LayoutContext): void {
|
||||
// Pass on space to the child, but only if the revealer is open. If not,
|
||||
// avoid forcing a re-layout of the child.
|
||||
if (this.openFractionBeforeEase > 0) {
|
||||
componentsById[this.state.content]!.allocatedWidth =
|
||||
this.allocatedWidth;
|
||||
}
|
||||
}
|
||||
|
||||
updateNaturalHeight(ctx: LayoutContext): void {
|
||||
this.naturalHeight = 0;
|
||||
|
||||
// Account for the header, if present
|
||||
if (this.state.header !== null) {
|
||||
this.naturalHeight +=
|
||||
this.labelHeight + 2 * HEADER_PADDING * this.headerScale;
|
||||
}
|
||||
|
||||
// Account for the content
|
||||
if (this.openFractionBeforeEase > 0) {
|
||||
let t = easeInOut(this.openFractionBeforeEase);
|
||||
let innerHeight =
|
||||
componentsById[this.state.content]!.requestedHeight;
|
||||
this.naturalHeight += t * innerHeight;
|
||||
}
|
||||
}
|
||||
|
||||
updateAllocatedHeight(ctx: LayoutContext): void {
|
||||
// Avoid forcing a re-layout of the child if the revealer is closed.
|
||||
if (this.openFractionBeforeEase === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Pass on space to the child
|
||||
let headerHeight =
|
||||
this.state.header === null
|
||||
? 0
|
||||
: this.labelHeight + 2 * HEADER_PADDING * this.headerScale;
|
||||
|
||||
let child = componentsById[this.state.content]!;
|
||||
child.allocatedHeight = Math.max(
|
||||
this.allocatedHeight - headerHeight,
|
||||
componentsById[this.state.content]!.requestedHeight
|
||||
);
|
||||
|
||||
// Position the child
|
||||
let element = child.element;
|
||||
element.style.left = '0';
|
||||
element.style.top = '0';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,248 +0,0 @@
|
||||
import { pixelsPerRem, scrollBarSize } from '../app';
|
||||
import { componentsById } from '../componentManagement';
|
||||
import { LayoutContext } from '../layouting';
|
||||
import { ComponentId } from '../dataModels';
|
||||
import { ComponentBase, ComponentState } 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;
|
||||
sticky_bottom?: boolean;
|
||||
};
|
||||
|
||||
const NATURAL_SIZE = 1.0;
|
||||
|
||||
export class ScrollContainerComponent extends ComponentBase {
|
||||
state: Required<ScrollContainerState>;
|
||||
|
||||
// Sometimes components are temporarily removed from the DOM or resized (for
|
||||
// example, `getElementDimensions` does both), which can lead to the scroll
|
||||
// position being changed or reset. In order to prevent this, we'll wrap our
|
||||
// child in a container element.
|
||||
private childContainer: HTMLElement;
|
||||
|
||||
private isFirstLayout: boolean = true;
|
||||
private assumeVerticalScrollBarWillBeNeeded: boolean = true;
|
||||
private numSequentialIncorrectAssumptions: number = 0;
|
||||
private wasScrolledToBottom: boolean | null = null;
|
||||
|
||||
private shouldLayoutWithVerticalScrollbar(): boolean {
|
||||
switch (this.state.scroll_y) {
|
||||
case 'always':
|
||||
return true;
|
||||
|
||||
case 'auto':
|
||||
return this.assumeVerticalScrollBarWillBeNeeded;
|
||||
|
||||
case 'never':
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
createElement(): HTMLElement {
|
||||
let element = document.createElement('div');
|
||||
element.classList.add('rio-scroll-container');
|
||||
|
||||
this.childContainer = document.createElement('div');
|
||||
element.appendChild(this.childContainer);
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
updateElement(
|
||||
deltaState: ScrollContainerState,
|
||||
latentComponents: Set<ComponentBase>
|
||||
): void {
|
||||
this.replaceOnlyChild(
|
||||
latentComponents,
|
||||
deltaState.content,
|
||||
this.childContainer
|
||||
);
|
||||
}
|
||||
|
||||
updateNaturalWidth(ctx: LayoutContext): void {
|
||||
if (this.state.scroll_x === 'never') {
|
||||
let child = componentsById[this.state.content]!;
|
||||
this.naturalWidth = child.requestedWidth;
|
||||
} else {
|
||||
this.naturalWidth = NATURAL_SIZE;
|
||||
}
|
||||
|
||||
// If there will be a vertical scroll bar, reserve space for it
|
||||
if (this.shouldLayoutWithVerticalScrollbar()) {
|
||||
this.naturalWidth += scrollBarSize;
|
||||
}
|
||||
}
|
||||
|
||||
updateAllocatedWidth(ctx: LayoutContext): void {
|
||||
let child = componentsById[this.state.content]!;
|
||||
|
||||
// If `sticky_bottom` is enabled, we need to find out whether we're
|
||||
// scrolled all the way to the bottom before we change the child's
|
||||
// allocation.
|
||||
//
|
||||
// We can't do this in `updateNaturalWidth` because the layouting
|
||||
// algorithm doesn't always call that method.
|
||||
if (
|
||||
this.state.sticky_bottom &&
|
||||
this.numSequentialIncorrectAssumptions === 0
|
||||
) {
|
||||
this.wasScrolledToBottom = this._checkIfScrolledToBottom(child);
|
||||
}
|
||||
|
||||
let availableWidth = this.allocatedWidth;
|
||||
if (this.shouldLayoutWithVerticalScrollbar()) {
|
||||
availableWidth -= scrollBarSize;
|
||||
}
|
||||
|
||||
// If the child needs more space than we have, we'll need to display a
|
||||
// scroll bar. So just give the child the width it wants.
|
||||
if (child.requestedWidth > availableWidth) {
|
||||
child.allocatedWidth = child.requestedWidth;
|
||||
this.element.style.overflowX = 'scroll';
|
||||
} else {
|
||||
// Otherwise, stretch the child to use up all the available width
|
||||
child.allocatedWidth = availableWidth;
|
||||
|
||||
this.element.style.overflowX =
|
||||
this.state.scroll_x === 'always' ? 'scroll' : 'hidden';
|
||||
}
|
||||
|
||||
this.childContainer.style.width = `${child.allocatedWidth}rem`;
|
||||
}
|
||||
|
||||
updateNaturalHeight(ctx: LayoutContext): void {
|
||||
if (this.state.scroll_y === 'never') {
|
||||
let child = componentsById[this.state.content]!;
|
||||
this.naturalHeight = child.requestedHeight;
|
||||
} else {
|
||||
this.naturalHeight = NATURAL_SIZE;
|
||||
}
|
||||
|
||||
// If there will be a horizontal scroll bar, reserve space for it
|
||||
if (this.element.style.overflowX === 'scroll') {
|
||||
this.naturalHeight += scrollBarSize;
|
||||
}
|
||||
}
|
||||
|
||||
updateAllocatedHeight(ctx: LayoutContext): void {
|
||||
let child = componentsById[this.state.content]!;
|
||||
|
||||
let availableHeight = this.allocatedHeight;
|
||||
if (this.element.style.overflowX === 'scroll') {
|
||||
availableHeight -= scrollBarSize;
|
||||
}
|
||||
|
||||
// If the child needs more space than we have, we'll need to display a
|
||||
// scroll bar. So just give the child the height it wants.
|
||||
let newAllocatedHeight: number;
|
||||
if (child.requestedHeight > availableHeight) {
|
||||
newAllocatedHeight = child.requestedHeight;
|
||||
this.element.style.overflowY = 'scroll';
|
||||
} else {
|
||||
// Otherwise, stretch the child to use up all the available height
|
||||
newAllocatedHeight = availableHeight;
|
||||
|
||||
if (this.state.scroll_y === 'always') {
|
||||
this.element.style.overflowY = 'scroll';
|
||||
} else {
|
||||
this.element.style.overflowY = 'hidden';
|
||||
}
|
||||
}
|
||||
|
||||
// Now check if our assumption for the vertical scroll bar was correct.
|
||||
// If not, we have to immediately re-layout the child.
|
||||
let hasVerticalScrollbar = this.element.style.overflowY === 'scroll';
|
||||
if (
|
||||
this.state.scroll_y === 'auto' &&
|
||||
this.assumeVerticalScrollBarWillBeNeeded !== hasVerticalScrollbar
|
||||
) {
|
||||
// Theoretically, there could be a situation where our assumptions
|
||||
// are always wrong and we re-layout endlessly.
|
||||
//
|
||||
// It's acceptable to have an unnecessary scroll bar, but it's not
|
||||
// acceptable to be missing a scroll bar when one is required. So we
|
||||
// will only re-layout if this is the first time our assumption was
|
||||
// wrong, or if we don't currently have a scroll bar.
|
||||
if (
|
||||
this.numSequentialIncorrectAssumptions == 0 ||
|
||||
!this.assumeVerticalScrollBarWillBeNeeded
|
||||
) {
|
||||
this.numSequentialIncorrectAssumptions++;
|
||||
this.assumeVerticalScrollBarWillBeNeeded =
|
||||
!this.assumeVerticalScrollBarWillBeNeeded;
|
||||
|
||||
// While a re-layout is about to be requested, this doesn't mean
|
||||
// that the current layouting process won't continue. Assign a
|
||||
// reasonable amount of space to the child so any subsequent
|
||||
// layouting functions don't crash because of unassigned values.
|
||||
//
|
||||
// The exact value doesn't matter, but this one is noticeable
|
||||
// when debugging and easy to find in the code.
|
||||
child.allocatedHeight = 7.77777777;
|
||||
|
||||
// Then, request a re-layout
|
||||
ctx.requestImmediateReLayout(() => {
|
||||
this.makeLayoutDirty();
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.numSequentialIncorrectAssumptions = 0;
|
||||
|
||||
// Only change the allocatedHeight once we're sure that we won't be
|
||||
// re-layouting again
|
||||
child.allocatedHeight = newAllocatedHeight;
|
||||
this.childContainer.style.height = `${child.allocatedHeight}rem`;
|
||||
|
||||
if (this.isFirstLayout) {
|
||||
this.isFirstLayout = false;
|
||||
|
||||
// Our CSS `height` hasn't been updated yet, so we can't scroll
|
||||
// down any further. We must assign the `height` manually.
|
||||
this.element.style.height = `${this.allocatedHeight}rem`;
|
||||
|
||||
this.element.scroll({
|
||||
top:
|
||||
(child.allocatedHeight - this.allocatedHeight) *
|
||||
pixelsPerRem *
|
||||
this.state.initial_y,
|
||||
left:
|
||||
(child.allocatedWidth - this.allocatedWidth) *
|
||||
pixelsPerRem *
|
||||
this.state.initial_x,
|
||||
behavior: 'instant',
|
||||
});
|
||||
}
|
||||
// If `sticky_bottom` is enabled, check if we have to scroll down
|
||||
else if (this.state.sticky_bottom && this.wasScrolledToBottom) {
|
||||
// Our CSS `height` hasn't been updated yet, so we can't scroll
|
||||
// down any further. We must assign the `height` manually.
|
||||
this.element.style.height = `${this.allocatedHeight}rem`;
|
||||
|
||||
this.element.scroll({
|
||||
top: child.allocatedHeight * pixelsPerRem + 999,
|
||||
left: this.element.scrollLeft,
|
||||
behavior: 'instant',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _checkIfScrolledToBottom(child: ComponentBase): boolean {
|
||||
// Calculate how much of the child is visible
|
||||
let visibleHeight = this.allocatedHeight;
|
||||
if (this.element.style.overflowX === 'scroll') {
|
||||
visibleHeight -= scrollBarSize;
|
||||
}
|
||||
|
||||
return (
|
||||
(this.element.scrollTop + 1) / pixelsPerRem + visibleHeight >=
|
||||
child.allocatedHeight - 0.00001
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,5 @@
|
||||
import {
|
||||
componentsById,
|
||||
tryGetComponentByElement,
|
||||
} from '../componentManagement';
|
||||
import { tryGetComponentByElement } from '../componentManagement';
|
||||
import { ComponentId } from '../dataModels';
|
||||
import { getTextDimensions } from '../layoutHelpers';
|
||||
import { LayoutContext } from '../layouting';
|
||||
import { setClipboard } from '../utils';
|
||||
import { ComponentBase, ComponentState } from './componentBase';
|
||||
|
||||
@@ -22,7 +17,6 @@ export class ScrollTargetComponent extends ComponentBase {
|
||||
|
||||
childContainerElement: HTMLElement;
|
||||
buttonContainerElement: HTMLElement;
|
||||
cachedButtonTextSize: [number, number];
|
||||
|
||||
createElement(): HTMLElement {
|
||||
let element = document.createElement('a');
|
||||
@@ -48,6 +42,8 @@ export class ScrollTargetComponent extends ComponentBase {
|
||||
deltaState: ScrollTargetState,
|
||||
latentComponents: Set<ComponentBase>
|
||||
): void {
|
||||
super.updateElement(deltaState, latentComponents);
|
||||
|
||||
this.replaceOnlyChild(
|
||||
latentComponents,
|
||||
deltaState.content,
|
||||
@@ -77,11 +73,6 @@ export class ScrollTargetComponent extends ComponentBase {
|
||||
let textElement = document.createElement('span');
|
||||
textElement.textContent = deltaState.copy_button_text;
|
||||
this.buttonContainerElement.appendChild(textElement);
|
||||
|
||||
this.cachedButtonTextSize = getTextDimensions(
|
||||
deltaState.copy_button_text,
|
||||
'text'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,91 +99,4 @@ export class ScrollTargetComponent extends ComponentBase {
|
||||
|
||||
setClipboard(url.toString());
|
||||
}
|
||||
|
||||
updateNaturalWidth(ctx: LayoutContext): void {
|
||||
if (this.state.content === null) {
|
||||
this.naturalWidth = 0;
|
||||
} else {
|
||||
this.naturalWidth =
|
||||
componentsById[this.state.content]!.requestedWidth;
|
||||
}
|
||||
|
||||
if (this.state.copy_button_content !== null) {
|
||||
this.naturalWidth +=
|
||||
componentsById[this.state.copy_button_content]!.requestedWidth;
|
||||
} else if (this.state.copy_button_text !== null) {
|
||||
this.naturalWidth += this.cachedButtonTextSize[0];
|
||||
}
|
||||
|
||||
// If both children exist, add the spacing
|
||||
if (
|
||||
this.state.content !== null &&
|
||||
(this.state.copy_button_content !== null ||
|
||||
this.state.copy_button_text !== null)
|
||||
) {
|
||||
this.naturalWidth += this.state.copy_button_spacing;
|
||||
}
|
||||
}
|
||||
|
||||
updateAllocatedWidth(ctx: LayoutContext): void {
|
||||
// The button component gets as much space as it requested, and the
|
||||
// other child gets all the rest
|
||||
let remainingWidth =
|
||||
this.allocatedWidth - this.state.copy_button_spacing;
|
||||
let buttonX = 0;
|
||||
|
||||
if (this.state.copy_button_content !== null) {
|
||||
let buttonComponent =
|
||||
componentsById[this.state.copy_button_content]!;
|
||||
|
||||
buttonComponent.allocatedWidth = buttonComponent.requestedWidth;
|
||||
remainingWidth -= buttonComponent.allocatedWidth;
|
||||
} else if (this.state.copy_button_text !== null) {
|
||||
remainingWidth -= this.cachedButtonTextSize[0];
|
||||
}
|
||||
|
||||
if (this.state.content !== null) {
|
||||
componentsById[this.state.content]!.allocatedWidth = remainingWidth;
|
||||
buttonX = remainingWidth + this.state.copy_button_spacing;
|
||||
}
|
||||
|
||||
if (
|
||||
this.state.copy_button_content !== null ||
|
||||
this.state.copy_button_text !== null
|
||||
) {
|
||||
let childElement = this.buttonContainerElement
|
||||
.firstElementChild as HTMLElement;
|
||||
childElement.style.left = `${buttonX}rem`;
|
||||
}
|
||||
}
|
||||
|
||||
updateNaturalHeight(ctx: LayoutContext): void {
|
||||
let contentHeight = 0;
|
||||
let copyButtonHeight = 0;
|
||||
|
||||
if (this.state.content !== null) {
|
||||
contentHeight = componentsById[this.state.content]!.requestedHeight;
|
||||
}
|
||||
|
||||
if (this.state.copy_button_content !== null) {
|
||||
copyButtonHeight =
|
||||
componentsById[this.state.copy_button_content]!.requestedHeight;
|
||||
} else if (this.state.copy_button_text !== null) {
|
||||
copyButtonHeight = this.cachedButtonTextSize[1];
|
||||
}
|
||||
|
||||
this.naturalHeight = Math.max(contentHeight, copyButtonHeight);
|
||||
}
|
||||
|
||||
updateAllocatedHeight(ctx: LayoutContext): void {
|
||||
if (this.state.content !== null) {
|
||||
componentsById[this.state.content]!.allocatedHeight =
|
||||
this.allocatedHeight;
|
||||
}
|
||||
|
||||
if (this.state.copy_button_content !== null) {
|
||||
componentsById[this.state.copy_button_content]!.allocatedHeight =
|
||||
this.allocatedHeight;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Color } from '../dataModels';
|
||||
import { ComponentBase, ComponentState } from './componentBase';
|
||||
import { pixelsPerRem } from '../app';
|
||||
import { LayoutContext } from '../layouting';
|
||||
import { colorToCssString } from '../cssUtils';
|
||||
|
||||
export type SeparatorState = ComponentState & {
|
||||
@@ -23,6 +22,8 @@ export class SeparatorComponent extends ComponentBase {
|
||||
deltaState: SeparatorState,
|
||||
latentComponents: Set<ComponentBase>
|
||||
): void {
|
||||
super.updateElement(deltaState, latentComponents);
|
||||
|
||||
// Color
|
||||
if (deltaState.color === undefined) {
|
||||
// Nothing to do
|
||||
@@ -43,18 +44,5 @@ export class SeparatorComponent extends ComponentBase {
|
||||
);
|
||||
this.element.style.setProperty('--separator-opacity', '1');
|
||||
}
|
||||
|
||||
// Orientation
|
||||
if (deltaState.orientation !== undefined) {
|
||||
this.makeLayoutDirty();
|
||||
}
|
||||
}
|
||||
|
||||
updateNaturalWidth(ctx: LayoutContext): void {
|
||||
this.naturalWidth = 1 / pixelsPerRem;
|
||||
}
|
||||
|
||||
updateNaturalHeight(ctx: LayoutContext): void {
|
||||
this.naturalHeight = 1 / pixelsPerRem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { ComponentBase, ComponentState } from './componentBase';
|
||||
import { LayoutContext } from '../layouting';
|
||||
|
||||
export type SeparatorListItemState = ComponentState & {
|
||||
_type_: 'SeparatorListItem-builtin';
|
||||
@@ -11,13 +10,4 @@ export class SeparatorListItemComponent extends ComponentBase {
|
||||
createElement(): HTMLElement {
|
||||
return document.createElement('div');
|
||||
}
|
||||
|
||||
updateElement(
|
||||
deltaState: SeparatorListItemState,
|
||||
latentComponents: Set<ComponentBase>
|
||||
): void {}
|
||||
|
||||
updateNaturalHeight(ctx: LayoutContext): void {
|
||||
this.naturalHeight = 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,42 +1,3 @@
|
||||
import { LayoutContext } from '../layouting';
|
||||
import { ComponentBase } from './componentBase';
|
||||
|
||||
export abstract class SingleContainer extends ComponentBase {
|
||||
updateNaturalWidth(ctx: LayoutContext): void {
|
||||
this.naturalWidth = 0;
|
||||
|
||||
for (let child of this.children) {
|
||||
this.naturalWidth = Math.max(
|
||||
this.naturalWidth,
|
||||
child.requestedWidth
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
updateAllocatedWidth(ctx: LayoutContext): void {
|
||||
for (let child of this.children) {
|
||||
child.allocatedWidth = this.allocatedWidth;
|
||||
}
|
||||
}
|
||||
|
||||
updateNaturalHeight(ctx: LayoutContext): void {
|
||||
this.naturalHeight = 0;
|
||||
|
||||
for (let child of this.children) {
|
||||
this.naturalHeight = Math.max(
|
||||
this.naturalHeight,
|
||||
child.requestedHeight
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
updateAllocatedHeight(ctx: LayoutContext): void {
|
||||
for (let child of this.children) {
|
||||
child.allocatedHeight = this.allocatedHeight;
|
||||
|
||||
let element = child.element;
|
||||
element.style.left = '0';
|
||||
element.style.top = '0';
|
||||
}
|
||||
}
|
||||
}
|
||||
export abstract class SingleContainer extends ComponentBase {}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { pixelsPerRem } from '../app';
|
||||
import { applySwitcheroo } from '../designApplication';
|
||||
import { LayoutContext } from '../layouting';
|
||||
import { firstDefined } from '../utils';
|
||||
import { ComponentBase, ComponentState } from './componentBase';
|
||||
|
||||
@@ -144,6 +142,8 @@ export class SliderComponent extends ComponentBase {
|
||||
deltaState: SliderState,
|
||||
latentComponents: Set<ComponentBase>
|
||||
): void {
|
||||
super.updateElement(deltaState, latentComponents);
|
||||
|
||||
if (
|
||||
deltaState.minimum !== undefined ||
|
||||
deltaState.maximum !== undefined ||
|
||||
@@ -192,16 +192,4 @@ export class SliderComponent extends ComponentBase {
|
||||
this.makeLayoutDirty();
|
||||
}
|
||||
}
|
||||
|
||||
updateNaturalWidth(ctx: LayoutContext): void {
|
||||
this.naturalWidth = 3;
|
||||
}
|
||||
|
||||
updateNaturalHeight(ctx: LayoutContext): void {
|
||||
this.naturalHeight = 1.4;
|
||||
|
||||
if (this.state.show_values) {
|
||||
this.naturalHeight += 0.9;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,6 +73,8 @@ export class SlideshowComponent extends SingleContainer {
|
||||
deltaState: SlideshowState,
|
||||
latentComponents: Set<ComponentBase>
|
||||
): void {
|
||||
super.updateElement(deltaState, latentComponents);
|
||||
|
||||
// Update the children
|
||||
if (deltaState.children !== undefined) {
|
||||
this.replaceChildren(
|
||||
|
||||
@@ -20,6 +20,8 @@ export class StackComponent extends SingleContainer {
|
||||
deltaState: StackState,
|
||||
latentComponents: Set<ComponentBase>
|
||||
): void {
|
||||
super.updateElement(deltaState, latentComponents);
|
||||
|
||||
this.replaceChildren(latentComponents, deltaState.children);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { LayoutContext } from '../layouting';
|
||||
import { ComponentBase, ComponentState } from './componentBase';
|
||||
|
||||
export type SwitchState = ComponentState & {
|
||||
@@ -38,6 +37,8 @@ export class SwitchComponent extends ComponentBase {
|
||||
deltaState: SwitchState,
|
||||
latentComponents: Set<ComponentBase>
|
||||
): void {
|
||||
super.updateElement(deltaState, latentComponents);
|
||||
|
||||
if (deltaState.is_on !== undefined) {
|
||||
if (deltaState.is_on) {
|
||||
this.element.classList.add('is-on');
|
||||
@@ -68,12 +69,4 @@ export class SwitchComponent extends ComponentBase {
|
||||
// identical. Make them look different. The switch animation also kinda
|
||||
// reacts to user input even if not sensitive.
|
||||
}
|
||||
|
||||
updateNaturalWidth(ctx: LayoutContext): void {
|
||||
this.naturalWidth = 3.18;
|
||||
}
|
||||
|
||||
updateNaturalHeight(ctx: LayoutContext): void {
|
||||
this.naturalHeight = 1.54;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { ComponentId } from '../dataModels';
|
||||
import { ComponentBase, ComponentState } from './componentBase';
|
||||
import { componentsById } from '../componentManagement';
|
||||
import { LayoutContext, updateLayout } from '../layouting';
|
||||
import { easeInOut } from '../easeFunctions';
|
||||
import { commitCss } from '../utils';
|
||||
|
||||
@@ -47,6 +46,8 @@ export class SwitcherComponent extends ComponentBase {
|
||||
deltaState: SwitcherState,
|
||||
latentComponents: Set<ComponentBase>
|
||||
): void {
|
||||
super.updateElement(deltaState, latentComponents);
|
||||
|
||||
// FIXME: Too low transition times cause issues for some reason.
|
||||
// Until that is fixed, clamp the value.
|
||||
if (
|
||||
@@ -138,9 +139,6 @@ export class SwitcherComponent extends ComponentBase {
|
||||
commitCss(this.activeChildContainer);
|
||||
this.activeChildContainer.classList.add('rio-switcher-active');
|
||||
}
|
||||
|
||||
// Start the layouting process
|
||||
this.makeLayoutDirty();
|
||||
}
|
||||
|
||||
// The component is now initialized
|
||||
@@ -154,149 +152,5 @@ export class SwitcherComponent extends ComponentBase {
|
||||
}
|
||||
|
||||
this.animationStartedAt = Date.now();
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
this.makeLayoutDirty();
|
||||
updateLayout();
|
||||
});
|
||||
}
|
||||
|
||||
updateNaturalWidth(ctx: LayoutContext): void {
|
||||
// If the child's requested size has changed, start the animation
|
||||
let childRequestedWidth: number, childRequestedHeight: number;
|
||||
|
||||
if (this.activeChildInstance === null) {
|
||||
childRequestedWidth = 0;
|
||||
childRequestedHeight = 0;
|
||||
} else {
|
||||
childRequestedWidth = this.activeChildInstance.requestedWidth;
|
||||
childRequestedHeight = this.activeChildInstance.requestedHeight;
|
||||
}
|
||||
|
||||
if (
|
||||
this.previousChildRequestedWidth !== childRequestedWidth ||
|
||||
this.previousChildRequestedHeight !== childRequestedHeight
|
||||
) {
|
||||
this.isDeterminingLayout = true;
|
||||
this.previousChildRequestedWidth = childRequestedWidth;
|
||||
this.previousChildRequestedHeight = childRequestedHeight;
|
||||
}
|
||||
|
||||
// Case: Trying to determine the size the child will receive once the
|
||||
// animation finishes
|
||||
if (this.isDeterminingLayout) {
|
||||
this.naturalWidth = childRequestedWidth;
|
||||
return;
|
||||
}
|
||||
|
||||
// Case: animated layouting
|
||||
let now = Date.now();
|
||||
let linearT = Math.min(
|
||||
1,
|
||||
(now - this.animationStartedAt) / 1000 / this.state.transition_time
|
||||
);
|
||||
let easedT = easeInOut(linearT);
|
||||
|
||||
this.naturalWidth =
|
||||
this.initialRequestedWidth +
|
||||
easedT * (childRequestedWidth - this.initialRequestedWidth);
|
||||
|
||||
this.naturalHeight =
|
||||
this.initialRequestedHeight +
|
||||
easedT * (childRequestedHeight - this.initialRequestedHeight);
|
||||
|
||||
// Keep going?
|
||||
if (linearT < 1) {
|
||||
requestAnimationFrame(() => {
|
||||
this.makeLayoutDirty();
|
||||
updateLayout();
|
||||
});
|
||||
} else {
|
||||
this.initialRequestedWidth = Math.max(
|
||||
this.naturalWidth,
|
||||
this.state._size_[0]
|
||||
);
|
||||
this.initialRequestedHeight = Math.max(
|
||||
this.naturalHeight,
|
||||
this.state._size_[1]
|
||||
);
|
||||
this.animationStartedAt = -1;
|
||||
}
|
||||
}
|
||||
|
||||
updateAllocatedWidth(ctx: LayoutContext): void {
|
||||
// Case: Trying to determine the size the child will receive once the
|
||||
// animation finishes
|
||||
// OR
|
||||
// Case: The parent component resized us
|
||||
if (this.isDeterminingLayout || this.animationStartedAt === -1) {
|
||||
if (this.activeChildInstance !== null) {
|
||||
this.activeChildInstance.allocatedWidth = this.allocatedWidth;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Case: animated layouting
|
||||
//
|
||||
// Nothing to do here
|
||||
}
|
||||
|
||||
updateNaturalHeight(ctx: LayoutContext): void {
|
||||
// Case: Trying to determine the size the child will receive once the
|
||||
// animation finishes
|
||||
if (this.isDeterminingLayout) {
|
||||
this.naturalHeight =
|
||||
this.activeChildInstance === null
|
||||
? 0
|
||||
: this.activeChildInstance.requestedHeight;
|
||||
return;
|
||||
}
|
||||
|
||||
// Case: animated layouting
|
||||
//
|
||||
// Already handled above
|
||||
}
|
||||
|
||||
updateAllocatedHeight(ctx: LayoutContext): void {
|
||||
// Case: Trying to determine the size the child will receive once the
|
||||
// animation finishes
|
||||
if (this.isDeterminingLayout) {
|
||||
if (this.activeChildInstance !== null) {
|
||||
this.activeChildInstance.allocatedHeight = this.allocatedHeight;
|
||||
}
|
||||
|
||||
this.isDeterminingLayout = false;
|
||||
|
||||
// If this is the first layout don't animate, just assume the
|
||||
// correct size
|
||||
if (!this.hasBeenLaidOut) {
|
||||
this.hasBeenLaidOut = true;
|
||||
this.initialRequestedWidth = this.allocatedWidth;
|
||||
this.initialRequestedHeight = this.allocatedHeight;
|
||||
return;
|
||||
}
|
||||
|
||||
// Start the animation
|
||||
if (this.animationStartedAt === -1) {
|
||||
this.animationStartedAt = Date.now();
|
||||
}
|
||||
|
||||
ctx.requestImmediateReLayout(() => {
|
||||
this.makeLayoutDirty();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Case: The parent component resized us
|
||||
if (this.animationStartedAt === -1) {
|
||||
if (this.activeChildInstance !== null) {
|
||||
this.activeChildInstance.allocatedHeight = this.allocatedHeight;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Case: animated layouting
|
||||
//
|
||||
// Nothing to do here
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { ComponentBase, ComponentState } from './componentBase';
|
||||
import { ColorSet } from '../dataModels';
|
||||
import { applySwitcheroo } from '../designApplication';
|
||||
import { getTextDimensionsWithCss } from '../layoutHelpers';
|
||||
import { LayoutContext } from '../layouting';
|
||||
import { easeInOut } from '../easeFunctions';
|
||||
import { firstDefined } from '../utils';
|
||||
|
||||
@@ -422,8 +420,9 @@ export class SwitcherBarComponent extends ComponentBase {
|
||||
deltaState: SwitcherBarState,
|
||||
latentComponents: Set<ComponentBase>
|
||||
): void {
|
||||
super.updateElement(deltaState, latentComponents);
|
||||
|
||||
let markerPositionNeedsUpdate = false;
|
||||
let needsReLayout = false;
|
||||
|
||||
// Have the options changed?
|
||||
if (
|
||||
@@ -487,7 +486,6 @@ export class SwitcherBarComponent extends ComponentBase {
|
||||
|
||||
// Request updates
|
||||
markerPositionNeedsUpdate = true;
|
||||
needsReLayout = true;
|
||||
}
|
||||
|
||||
// Color
|
||||
@@ -509,13 +507,11 @@ export class SwitcherBarComponent extends ComponentBase {
|
||||
|
||||
// Request updates
|
||||
markerPositionNeedsUpdate = true;
|
||||
needsReLayout = true;
|
||||
}
|
||||
|
||||
// Spacing
|
||||
if (deltaState.spacing !== undefined) {
|
||||
markerPositionNeedsUpdate = true;
|
||||
needsReLayout = true;
|
||||
}
|
||||
|
||||
// If the selection has changed make sure to move the marker
|
||||
@@ -538,70 +534,5 @@ export class SwitcherBarComponent extends ComponentBase {
|
||||
if (markerPositionNeedsUpdate) {
|
||||
this.moveMarkerInstantlyIfAnimationIsntRunning();
|
||||
}
|
||||
|
||||
if (needsReLayout) {
|
||||
this.makeLayoutDirty();
|
||||
}
|
||||
}
|
||||
|
||||
updateNaturalWidth(ctx: LayoutContext): void {
|
||||
if (this.state.orientation == 'horizontal') {
|
||||
// Spacing
|
||||
this.naturalWidth =
|
||||
this.state.spacing * (this.state.names.length - 1);
|
||||
|
||||
// Options
|
||||
this.optionWidths.forEach((width) => {
|
||||
this.naturalWidth += width;
|
||||
});
|
||||
} else {
|
||||
// Options
|
||||
this.naturalWidth = 0;
|
||||
|
||||
this.optionWidths.forEach((width) => {
|
||||
this.naturalWidth = Math.max(this.naturalWidth, width);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
updateNaturalHeight(ctx: LayoutContext): void {
|
||||
if (this.state.orientation == 'horizontal') {
|
||||
// Options
|
||||
this.naturalHeight = 0;
|
||||
|
||||
this.optionHeights.forEach((height) => {
|
||||
this.naturalHeight = Math.max(this.naturalHeight, height);
|
||||
});
|
||||
} else {
|
||||
// Spacing
|
||||
this.naturalHeight =
|
||||
this.state.spacing * (this.state.names.length - 1);
|
||||
|
||||
// Options
|
||||
this.optionHeights.forEach((height) => {
|
||||
this.naturalHeight += height;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
updateAllocatedHeight(ctx: LayoutContext): void {
|
||||
// Pass on the allocated size to the options
|
||||
let width, height;
|
||||
if (this.state.orientation == 'horizontal') {
|
||||
width = `${this.allocatedWidth}rem`;
|
||||
height = '';
|
||||
} else {
|
||||
width = '';
|
||||
height = `${this.allocatedHeight}rem`;
|
||||
}
|
||||
|
||||
this.backgroundOptionsElement.style.width = width;
|
||||
this.backgroundOptionsElement.style.height = height;
|
||||
|
||||
this.markerOptionsElement.style.width = width;
|
||||
this.markerOptionsElement.style.height = height;
|
||||
|
||||
// Reposition the marker
|
||||
this.moveMarkerInstantlyIfAnimationIsntRunning();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { getElementDimensions } from '../layoutHelpers';
|
||||
import { LayoutContext } from '../layouting';
|
||||
import { ComponentBase, ComponentState } from './componentBase';
|
||||
|
||||
type TableValue = number | string;
|
||||
@@ -139,6 +137,8 @@ export class TableComponent extends ComponentBase {
|
||||
deltaState: TableDeltaState,
|
||||
latentComponents: Set<ComponentBase>
|
||||
): void {
|
||||
super.updateElement(deltaState, latentComponents);
|
||||
|
||||
if (deltaState.data !== undefined) {
|
||||
this.state.data = dataToColumns(deltaState.data);
|
||||
|
||||
@@ -163,11 +163,6 @@ export class TableComponent extends ComponentBase {
|
||||
this.hideRowNumbers();
|
||||
}
|
||||
}
|
||||
|
||||
this.makeLayoutDirty();
|
||||
[this.naturalWidth, this.naturalHeight] = getElementDimensions(
|
||||
this.tableElement
|
||||
);
|
||||
}
|
||||
|
||||
private displayData(): void {
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { TextStyle } from '../dataModels';
|
||||
import { textStyleToCss } from '../cssUtils';
|
||||
import { ComponentBase, ComponentState } from './componentBase';
|
||||
import { LayoutContext } from '../layouting';
|
||||
import { getTextDimensions } from '../layoutHelpers';
|
||||
|
||||
export type TextState = ComponentState & {
|
||||
_type_: 'Text-builtin';
|
||||
@@ -33,6 +31,8 @@ export class TextComponent extends ComponentBase {
|
||||
deltaState: TextState,
|
||||
latentComponents: Set<ComponentBase>
|
||||
): void {
|
||||
super.updateElement(deltaState, latentComponents);
|
||||
|
||||
// BEFORE WE DO ANYTHING ELSE, update the text style
|
||||
if (deltaState.style !== undefined) {
|
||||
// Change the element to <h1>, <h2>, <h3> or <span> as necessary
|
||||
@@ -101,42 +101,5 @@ export class TextComponent extends ComponentBase {
|
||||
if (deltaState.justify !== undefined) {
|
||||
this.inner.style.textAlign = deltaState.justify;
|
||||
}
|
||||
|
||||
if (
|
||||
deltaState.text !== undefined ||
|
||||
deltaState.wrap !== undefined ||
|
||||
deltaState.style !== undefined
|
||||
) {
|
||||
this.makeLayoutDirty();
|
||||
|
||||
// Compute and cache the dimensions that our text requires if line
|
||||
// wrapping is disabled
|
||||
this.cachedNoWrapDimensions = getTextDimensions(
|
||||
this.element.textContent!,
|
||||
this.state.style
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
updateNaturalWidth(ctx: LayoutContext): void {
|
||||
if (this.state.wrap === false) {
|
||||
this.naturalWidth = this.cachedNoWrapDimensions[0];
|
||||
} else {
|
||||
this.naturalWidth = 0;
|
||||
}
|
||||
}
|
||||
|
||||
updateNaturalHeight(ctx: LayoutContext): void {
|
||||
if (this.state.wrap === true) {
|
||||
// Calculate how much height we need given the allocated width
|
||||
this.naturalHeight = getTextDimensions(
|
||||
this.state.text,
|
||||
this.state.style,
|
||||
this.allocatedWidth
|
||||
)[1];
|
||||
} else {
|
||||
// 'wrap' and 'ellipsize' both require the same height
|
||||
this.naturalHeight = this.cachedNoWrapDimensions[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,4 @@
|
||||
import { ComponentBase, ComponentState } from './componentBase';
|
||||
import { getTextDimensions } from '../layoutHelpers';
|
||||
import { LayoutContext } from '../layouting';
|
||||
import {
|
||||
HORIZONTAL_PADDING as INPUT_BOX_HORIZONTAL_PADDING,
|
||||
updateInputBoxNaturalHeight,
|
||||
updateInputBoxNaturalWidth,
|
||||
} from '../inputBoxTools';
|
||||
import { Debouncer } from '../debouncer';
|
||||
|
||||
export type TextInputState = ComponentState & {
|
||||
@@ -27,9 +20,6 @@ export class TextInputComponent extends ComponentBase {
|
||||
private prefixTextElement: HTMLElement;
|
||||
private suffixTextElement: HTMLElement;
|
||||
|
||||
private prefixTextWidth: number = 0;
|
||||
private suffixTextWidth: number = 0;
|
||||
|
||||
onChangeLimiter: Debouncer;
|
||||
|
||||
createElement(): HTMLElement {
|
||||
@@ -162,47 +152,30 @@ export class TextInputComponent extends ComponentBase {
|
||||
deltaState: TextInputState,
|
||||
latentComponents: Set<ComponentBase>
|
||||
): void {
|
||||
super.updateElement(deltaState, latentComponents);
|
||||
|
||||
if (deltaState.text !== undefined) {
|
||||
this.inputElement.value = deltaState.text;
|
||||
}
|
||||
|
||||
if (deltaState.label !== undefined) {
|
||||
this.labelElement.textContent = deltaState.label;
|
||||
|
||||
// Update the layout
|
||||
updateInputBoxNaturalHeight(this, deltaState.label, 0);
|
||||
}
|
||||
|
||||
if (deltaState.prefix_text === '') {
|
||||
this.prefixTextElement.style.display = 'none';
|
||||
this.prefixTextWidth = 0;
|
||||
this.inputElement.style.paddingLeft = `${INPUT_BOX_HORIZONTAL_PADDING}rem`;
|
||||
this.makeLayoutDirty();
|
||||
} else if (deltaState.prefix_text !== undefined) {
|
||||
this.prefixTextElement.textContent = deltaState.prefix_text;
|
||||
this.prefixTextElement.style.removeProperty('display');
|
||||
this.inputElement.style.removeProperty('padding-left');
|
||||
|
||||
// Update the layout, if needed
|
||||
this.prefixTextWidth =
|
||||
getTextDimensions(deltaState.prefix_text, 'text')[0] + 0.2;
|
||||
this.makeLayoutDirty();
|
||||
}
|
||||
|
||||
if (deltaState.suffix_text === '') {
|
||||
this.suffixTextElement.style.display = 'none';
|
||||
this.suffixTextWidth = 0;
|
||||
this.inputElement.style.paddingRight = `${INPUT_BOX_HORIZONTAL_PADDING}rem`;
|
||||
this.makeLayoutDirty();
|
||||
} else if (deltaState.suffix_text !== undefined) {
|
||||
this.suffixTextElement.textContent = deltaState.suffix_text;
|
||||
this.suffixTextElement.style.removeProperty('display');
|
||||
this.inputElement.style.removeProperty('padding-right');
|
||||
|
||||
// Update the layout, if needed
|
||||
this.suffixTextWidth =
|
||||
getTextDimensions(deltaState.suffix_text, 'text')[0] + 0.2;
|
||||
this.makeLayoutDirty();
|
||||
}
|
||||
|
||||
if (deltaState.is_secret !== undefined) {
|
||||
@@ -230,16 +203,4 @@ export class TextInputComponent extends ComponentBase {
|
||||
grabKeyboardFocus(): void {
|
||||
this.inputElement.focus();
|
||||
}
|
||||
|
||||
updateNaturalWidth(ctx: LayoutContext): void {
|
||||
updateInputBoxNaturalWidth(
|
||||
this,
|
||||
this.prefixTextWidth + this.suffixTextWidth
|
||||
);
|
||||
}
|
||||
|
||||
updateNaturalHeight(ctx: LayoutContext): void {
|
||||
// This is set during the updateElement() call, so there is nothing to
|
||||
// do here.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@ export class ThemeContextSwitcherComponent extends SingleContainer {
|
||||
deltaState: ThemeContextSwitcherState,
|
||||
latentComponents: Set<ComponentBase>
|
||||
): void {
|
||||
super.updateElement(deltaState, latentComponents);
|
||||
|
||||
// Update the child
|
||||
this.replaceOnlyChild(latentComponents, deltaState.content);
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { componentsById } from '../componentManagement';
|
||||
import { LayoutContext } from '../layouting';
|
||||
import { ComponentId } from '../dataModels';
|
||||
import { ComponentBase, ComponentState } from './componentBase';
|
||||
import { PopupManager } from '../popupManager';
|
||||
@@ -64,6 +63,8 @@ export class TooltipComponent extends ComponentBase {
|
||||
deltaState: TooltipState,
|
||||
latentComponents: Set<ComponentBase>
|
||||
): void {
|
||||
super.updateElement(deltaState, latentComponents);
|
||||
|
||||
// Update the anchor
|
||||
if (deltaState.anchor !== undefined) {
|
||||
this.replaceOnlyChild(
|
||||
@@ -98,36 +99,4 @@ export class TooltipComponent extends ComponentBase {
|
||||
|
||||
this.popupManager.destroy();
|
||||
}
|
||||
|
||||
updateNaturalWidth(ctx: LayoutContext): void {
|
||||
this.naturalWidth = componentsById[this.state.anchor!]!.requestedWidth;
|
||||
}
|
||||
|
||||
updateAllocatedWidth(ctx: LayoutContext): void {
|
||||
let anchor = componentsById[this.state.anchor!]!;
|
||||
let tip = componentsById[this.state._tip_component!]!;
|
||||
|
||||
anchor.allocatedWidth = this.allocatedWidth;
|
||||
tip.allocatedWidth = tip.requestedWidth;
|
||||
}
|
||||
|
||||
updateNaturalHeight(ctx: LayoutContext): void {
|
||||
this.naturalHeight =
|
||||
componentsById[this.state.anchor!]!.requestedHeight;
|
||||
}
|
||||
|
||||
updateAllocatedHeight(ctx: LayoutContext): void {
|
||||
let anchor = componentsById[this.state.anchor!]!;
|
||||
let tip = componentsById[this.state._tip_component!]!;
|
||||
|
||||
anchor.allocatedHeight = this.allocatedHeight;
|
||||
tip.allocatedHeight = tip.requestedHeight;
|
||||
|
||||
// Position the children
|
||||
anchor.element.style.left = '0';
|
||||
anchor.element.style.top = '0';
|
||||
|
||||
tip.element.style.left = '0';
|
||||
tip.element.style.top = '0';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@ export class WebsiteComponent extends ComponentBase {
|
||||
deltaState: WebsiteState,
|
||||
latentComponents: Set<ComponentBase>
|
||||
): void {
|
||||
super.updateElement(deltaState, latentComponents);
|
||||
|
||||
if (
|
||||
deltaState.url !== undefined &&
|
||||
deltaState.url !== this.element.src
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
export type ComponentId = number & { __brand: 'ComponentId' };
|
||||
|
||||
export type RioScrollBehavior = 'never' | 'auto' | 'always';
|
||||
|
||||
export type Color = [number, number, number, number];
|
||||
|
||||
export const COLOR_SET_NAMES = [
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/// don't exist. This module contains the logic for walking the tree and
|
||||
/// filtering out the nodes that shouldn't be displayed.
|
||||
|
||||
import { getRootScroller, componentsById } from './componentManagement';
|
||||
import { componentsById } from './componentManagement';
|
||||
import { ComponentBase } from './components/componentBase';
|
||||
|
||||
/// Many of the spawned components are internal to Rio and shouldn't be
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
// Several components share the same overall style: The input box.
|
||||
//
|
||||
// This file contains tools helpful for implementing them.
|
||||
|
||||
import { ComponentBase } from './components/componentBase';
|
||||
|
||||
export const HORIZONTAL_PADDING: number = 0.8;
|
||||
|
||||
export function updateInputBoxNaturalWidth(
|
||||
component: ComponentBase,
|
||||
additionalSpace: number
|
||||
): void {
|
||||
// Enforce a minimum width, common to all input boxes
|
||||
let newWidth = Math.max(8, additionalSpace + HORIZONTAL_PADDING * 2);
|
||||
|
||||
// Dirty?
|
||||
if (newWidth !== component.naturalWidth) {
|
||||
component.naturalWidth = newWidth;
|
||||
component.makeLayoutDirty();
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the component's natural height property, and make the layout dirty
|
||||
/// if needed.
|
||||
export function updateInputBoxNaturalHeight(
|
||||
component: ComponentBase,
|
||||
label: string,
|
||||
additionalSpace: number
|
||||
) {
|
||||
// Calculate the new height. If a label is set, the height needs to increase
|
||||
// to make room for it, when floating above the entered text.
|
||||
let newHeight = label.length === 0 ? 2.375 : 3.3;
|
||||
newHeight += additionalSpace;
|
||||
|
||||
// Dirty?
|
||||
if (newHeight !== component.naturalHeight) {
|
||||
component.naturalHeight = newHeight;
|
||||
component.makeLayoutDirty();
|
||||
}
|
||||
}
|
||||
@@ -1,169 +0,0 @@
|
||||
import { pixelsPerRem } from './app';
|
||||
import { textStyleToCss } from './cssUtils';
|
||||
import { TextStyle } from './dataModels';
|
||||
|
||||
const _textDimensionsCache = new Map<string, [number, number]>();
|
||||
|
||||
let cacheHits: number = 0;
|
||||
let cacheMisses: number = 0;
|
||||
|
||||
/// Returns the width and height of the given text in pixels. Caches the result.
|
||||
export function getTextDimensions(
|
||||
text: string,
|
||||
style: 'heading1' | 'heading2' | 'heading3' | 'text' | 'dim' | TextStyle,
|
||||
restrictWidth: number | null = null
|
||||
): [number, number] {
|
||||
// Make sure the text isn't just whitespace, as that results in a wrong [0,
|
||||
// 0]
|
||||
//
|
||||
// FIXME: Still imperfect, as now all whitespace is the same width, and an
|
||||
// empty string has a width.
|
||||
if (text.trim().length === 0) {
|
||||
text = 'l';
|
||||
}
|
||||
|
||||
// Build a key for the cache
|
||||
let key: string;
|
||||
let sizeNormalizationFactor: number;
|
||||
if (typeof style === 'string') {
|
||||
key = `${style}+${text}`;
|
||||
sizeNormalizationFactor = 1;
|
||||
} else {
|
||||
key = `${style.fontName}+${style.fontWeight}+${style.italic}+${style.underlined}+${style.allCaps}+${text}`;
|
||||
sizeNormalizationFactor = style.fontSize;
|
||||
}
|
||||
|
||||
// Restrict the width?
|
||||
if (restrictWidth !== null) {
|
||||
key += `+${restrictWidth}`;
|
||||
}
|
||||
|
||||
// Check the cache
|
||||
let cached = _textDimensionsCache.get(key);
|
||||
|
||||
if (cached !== undefined) {
|
||||
cacheHits++;
|
||||
return [
|
||||
cached[0] * sizeNormalizationFactor,
|
||||
cached[1] * sizeNormalizationFactor,
|
||||
];
|
||||
}
|
||||
cacheMisses++;
|
||||
|
||||
let result = getTextDimensionsWithCss(
|
||||
text,
|
||||
textStyleToCss(style),
|
||||
restrictWidth
|
||||
);
|
||||
|
||||
// Store in the cache and return
|
||||
_textDimensionsCache.set(key, [
|
||||
result[0] / sizeNormalizationFactor,
|
||||
result[1] / sizeNormalizationFactor,
|
||||
]);
|
||||
return result;
|
||||
}
|
||||
|
||||
export function getTextDimensionsWithCss(
|
||||
text: string,
|
||||
style: object,
|
||||
restrictWidth: number | null = null
|
||||
): [number, number] {
|
||||
let element = document.createElement('div');
|
||||
element.textContent = text;
|
||||
Object.assign(element.style, style);
|
||||
element.style.position = 'absolute';
|
||||
element.style.whiteSpace = 'pre-wrap'; // Required for multi-line text
|
||||
document.body.appendChild(element);
|
||||
|
||||
if (restrictWidth !== null) {
|
||||
element.style.width = `${restrictWidth}rem`;
|
||||
}
|
||||
|
||||
let rect = element.getBoundingClientRect();
|
||||
let result = [rect.width / pixelsPerRem, rect.height / pixelsPerRem] as [
|
||||
number,
|
||||
number,
|
||||
];
|
||||
element.remove();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
globalThis.getTextDimensions = getTextDimensions; // For debugging
|
||||
|
||||
/// Get the width and height an element takes up on the screen, in rems.
|
||||
///
|
||||
/// This works even if the element is not visible, e.g. because a parent is
|
||||
/// hidden.
|
||||
export function getElementDimensions(element: HTMLElement): [number, number] {
|
||||
let result: [number, number];
|
||||
|
||||
for (const _ of prepareElementForGetDimensions(element)) {
|
||||
result = [
|
||||
element.scrollWidth / pixelsPerRem,
|
||||
element.scrollHeight / pixelsPerRem,
|
||||
];
|
||||
}
|
||||
|
||||
// @ts-ignore ("used before assignment")
|
||||
return result;
|
||||
}
|
||||
|
||||
globalThis.getElementDimensions = getElementDimensions; // For debugging
|
||||
|
||||
export function getElementWidth(element: HTMLElement): number {
|
||||
let result: number;
|
||||
|
||||
for (const _ of prepareElementForGetDimensions(element)) {
|
||||
result = element.scrollWidth / pixelsPerRem;
|
||||
}
|
||||
|
||||
// @ts-ignore ("used before assignment")
|
||||
return result;
|
||||
}
|
||||
|
||||
export function getElementHeight(element: HTMLElement): number {
|
||||
let result: number;
|
||||
|
||||
for (const _ of prepareElementForGetDimensions(element)) {
|
||||
result = element.scrollHeight / pixelsPerRem;
|
||||
}
|
||||
|
||||
// @ts-ignore ("used before assignment")
|
||||
return result;
|
||||
}
|
||||
|
||||
function* prepareElementForGetDimensions(element: HTMLElement) {
|
||||
// Ensure the element is in the DOM
|
||||
let isInDom = element.isConnected;
|
||||
|
||||
let parentElement: HTMLElement | null = null;
|
||||
let nextSibling: Node | null = null;
|
||||
if (!isInDom) {
|
||||
parentElement = element.parentElement;
|
||||
nextSibling = element.nextSibling;
|
||||
document.body.appendChild(element);
|
||||
}
|
||||
|
||||
// Ensure it doesn't feel compelled to fill the entire parent element
|
||||
let originalPosition = element.style.position;
|
||||
element.style.position = 'fixed';
|
||||
|
||||
try {
|
||||
yield;
|
||||
} finally {
|
||||
// Restore the original state
|
||||
element.style.position = originalPosition;
|
||||
|
||||
if (!isInDom) {
|
||||
if (parentElement === null) {
|
||||
element.remove();
|
||||
} else if (nextSibling === null) {
|
||||
parentElement.appendChild(element);
|
||||
} else {
|
||||
parentElement.insertBefore(element, nextSibling);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,157 +0,0 @@
|
||||
import { pixelsPerRem } from './app';
|
||||
import { getRootComponent } from './componentManagement';
|
||||
import { ComponentBase } from './components/componentBase';
|
||||
import { DevToolsConnectorComponent } from './components/devToolsConnector';
|
||||
|
||||
export class LayoutContext {
|
||||
_immediateReLayoutCallbacks: (() => void)[] = [];
|
||||
|
||||
private updateRequestedWidthRecursive(component: ComponentBase): void {
|
||||
if (!component.isLayoutDirty) return;
|
||||
|
||||
for (let child of component.children) {
|
||||
this.updateRequestedWidthRecursive(child);
|
||||
}
|
||||
|
||||
component.updateNaturalWidth(this);
|
||||
component.requestedWidth = Math.max(
|
||||
component.naturalWidth,
|
||||
component.state._size_[0]
|
||||
);
|
||||
}
|
||||
|
||||
private updateAllocatedWidthRecursive(component: ComponentBase): void {
|
||||
if (!component.isLayoutDirty) return;
|
||||
|
||||
let children = Array.from(component.children);
|
||||
let childAllocatedWidths = children.map(
|
||||
(child) => child.allocatedWidth
|
||||
);
|
||||
|
||||
component.updateAllocatedWidth(this);
|
||||
|
||||
// The FundamentalRootComponent always has a width of 100vw, so we don't
|
||||
// want to assign the width here. We'll only assign the width of this
|
||||
// component's children.
|
||||
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
let child = children[i];
|
||||
|
||||
if (child.allocatedWidth !== childAllocatedWidths[i]) {
|
||||
child.isLayoutDirty = true;
|
||||
}
|
||||
|
||||
if (child.isLayoutDirty) {
|
||||
this.updateAllocatedWidthRecursive(child);
|
||||
}
|
||||
|
||||
let element = child.element;
|
||||
element.style.width = `${child.allocatedWidth * pixelsPerRem}px`;
|
||||
}
|
||||
}
|
||||
|
||||
private updateRequestedHeightRecursive(component: ComponentBase): void {
|
||||
if (!component.isLayoutDirty) return;
|
||||
|
||||
for (let child of component.children) {
|
||||
this.updateRequestedHeightRecursive(child);
|
||||
}
|
||||
|
||||
component.updateNaturalHeight(this);
|
||||
component.requestedHeight = Math.max(
|
||||
component.naturalHeight,
|
||||
component.state._size_[1]
|
||||
);
|
||||
}
|
||||
|
||||
private updateAllocatedHeightRecursive(component: ComponentBase): void {
|
||||
if (!component.isLayoutDirty) return;
|
||||
|
||||
let children = Array.from(component.children);
|
||||
let childAllocatedHeights = children.map(
|
||||
(child) => child.allocatedHeight
|
||||
);
|
||||
|
||||
component.updateAllocatedHeight(this);
|
||||
|
||||
// The FundamentalRootComponent always has a height of 100vh, so we
|
||||
// don't want to assign the width here. We'll only assign the height of
|
||||
// this component's children.
|
||||
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
let child = children[i];
|
||||
|
||||
if (child.allocatedHeight !== childAllocatedHeights[i]) {
|
||||
child.isLayoutDirty = true;
|
||||
}
|
||||
|
||||
if (child.isLayoutDirty) {
|
||||
this.updateAllocatedHeightRecursive(child);
|
||||
}
|
||||
|
||||
child.isLayoutDirty = false;
|
||||
|
||||
let element = child.element;
|
||||
element.style.height = `${child.allocatedHeight * pixelsPerRem}px`;
|
||||
}
|
||||
}
|
||||
|
||||
public updateLayout(): void {
|
||||
let rootComponent = getRootComponent();
|
||||
|
||||
// Find out how large all components would like to be
|
||||
this.updateRequestedWidthRecursive(rootComponent);
|
||||
|
||||
// Note: The FundamentalRootComponent is responsible for allocating the
|
||||
// available window space. There is no need to take care of anything
|
||||
// here.
|
||||
|
||||
// Distribute the just received width to all children
|
||||
this.updateAllocatedWidthRecursive(rootComponent);
|
||||
|
||||
// Now that all components have their width set, find out their height.
|
||||
// This is done later on, so that text can request height based on its
|
||||
// width.
|
||||
this.updateRequestedHeightRecursive(rootComponent);
|
||||
|
||||
// Distribute the just received height to all children
|
||||
this.updateAllocatedHeightRecursive(rootComponent);
|
||||
}
|
||||
|
||||
/// Signal to the layout engine that it should re-layout the component tree
|
||||
/// immediately after the current layout cycle finishes. The given function
|
||||
/// will be called before the re-layout happens, allowing the caller to
|
||||
/// dirty components or do other things.
|
||||
public requestImmediateReLayout(callback: () => void): void {
|
||||
this._immediateReLayoutCallbacks.push(callback);
|
||||
}
|
||||
}
|
||||
|
||||
export function updateLayout(): void {
|
||||
let context = new LayoutContext();
|
||||
|
||||
while (true) {
|
||||
// Update the layout
|
||||
context.updateLayout();
|
||||
|
||||
// Are any re-layouts requested?
|
||||
if (context._immediateReLayoutCallbacks.length === 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Call all hooks
|
||||
for (let callback of context._immediateReLayoutCallbacks) {
|
||||
callback();
|
||||
}
|
||||
|
||||
context._immediateReLayoutCallbacks = [];
|
||||
}
|
||||
|
||||
// Notify the dev tools, if any
|
||||
if (globalThis.RIO_DEV_TOOLS !== null) {
|
||||
let devToolsComponent =
|
||||
globalThis.RIO_DEV_TOOLS as DevToolsConnectorComponent;
|
||||
|
||||
devToolsComponent.afterLayoutUpdate();
|
||||
}
|
||||
}
|
||||
@@ -101,6 +101,22 @@ export function firstDefined(...args: any[]): any {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/// Removes `oldElement` from the DOM and inserts `newElement` at its position
|
||||
export function replaceElement(oldElement: Element, newElement: Node): void {
|
||||
oldElement.parentElement?.insertBefore(newElement, oldElement);
|
||||
oldElement.remove();
|
||||
}
|
||||
|
||||
/// Wraps the given Element in a DIV
|
||||
export function insertWrapperElement(wrappedElement: Element): HTMLElement {
|
||||
let wrapperElement = document.createElement('div');
|
||||
|
||||
replaceElement(wrappedElement, wrapperElement);
|
||||
wrapperElement.appendChild(wrappedElement);
|
||||
|
||||
return wrapperElement;
|
||||
}
|
||||
|
||||
/// Adds a timeout to a promise. Throws TimeoutError if the time limit is
|
||||
/// exceeded before the promise resolves.
|
||||
export function timeout<T>(
|
||||
@@ -356,7 +372,7 @@ export async function getComponentLayouts(
|
||||
}
|
||||
|
||||
// And its parent
|
||||
let parentComponent = component.getParentExcludingInjected()!;
|
||||
let parentComponent = component.getParent()!;
|
||||
|
||||
if (parentComponent === null) {
|
||||
result.push(null);
|
||||
|
||||
@@ -1,12 +1,27 @@
|
||||
@use 'sass:meta';
|
||||
@use 'switcheroos.scss';
|
||||
|
||||
@mixin single-container {
|
||||
display: inline-grid;
|
||||
}
|
||||
|
||||
@mixin center-content {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/// Kills the element's size request so it can't make its parent element grow.
|
||||
/// Always takes the size of the nearest *positioned* parent element.
|
||||
@mixin kill-size-request {
|
||||
// Prevent it from making the parent element grow
|
||||
position: absolute;
|
||||
|
||||
// Fill the entire parent
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
// Light / Dark highlight.js themes
|
||||
//
|
||||
// Switch between these by setting the `data-theme` attribute on the `html`
|
||||
@@ -109,15 +124,19 @@ code {
|
||||
html {
|
||||
background: var(--rio-global-background-bg);
|
||||
|
||||
// Don't scroll under any circumstances. Our layouting system takes care of
|
||||
// scrolling.
|
||||
overflow: hidden;
|
||||
// Fill the whole screen, at least
|
||||
min-width: 100%;
|
||||
min-height: 100%;
|
||||
|
||||
@include single-container;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
@include single-container;
|
||||
|
||||
font-family: var(--rio-global-font, sans-serif);
|
||||
}
|
||||
|
||||
@@ -130,27 +149,65 @@ select {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
// All components
|
||||
.rio-component {
|
||||
position: absolute;
|
||||
// Alignment helper elements
|
||||
.rio-align-outer {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.rio-align-inner {
|
||||
position: relative;
|
||||
|
||||
&.stretch-child-x > * {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&.stretch-child-y > * {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
// Container elements for child components
|
||||
.rio-child-wrapper {
|
||||
@include single-container();
|
||||
}
|
||||
|
||||
// User-defined components
|
||||
.rio-placeholder {
|
||||
@include single-container();
|
||||
}
|
||||
|
||||
// Fundamental Root Component
|
||||
.rio-fundamental-root-component {
|
||||
position: relative !important;
|
||||
display: inline-grid;
|
||||
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
|
||||
& > .rio-scroll-container {
|
||||
// The user's root component
|
||||
& > *:first-child {
|
||||
z-index: $z-index-user-root;
|
||||
|
||||
grid-row: 1;
|
||||
grid-column: 1;
|
||||
}
|
||||
|
||||
// The connection lost popup
|
||||
& > *:nth-child(2) {
|
||||
z-index: $z-index-error-popup;
|
||||
|
||||
grid-row: 1;
|
||||
grid-column: 1;
|
||||
}
|
||||
|
||||
// The dev tools sidebar
|
||||
& > *:nth-child(3) {
|
||||
z-index: $z-index-dev-tools;
|
||||
|
||||
grid-row: 1;
|
||||
grid-column: 2;
|
||||
}
|
||||
}
|
||||
|
||||
// Dev Tools
|
||||
.rio-dev-tools {
|
||||
pointer-events: auto;
|
||||
z-index: $z-index-dev-tools;
|
||||
}
|
||||
|
||||
.rio-dev-tools-hidden::after {
|
||||
@@ -1034,12 +1091,16 @@ textarea:not(:placeholder-shown) ~ .rio-input-box-label,
|
||||
// MediaPlayer
|
||||
.rio-media-player {
|
||||
pointer-events: auto;
|
||||
|
||||
position: relative;
|
||||
|
||||
@include center-content();
|
||||
}
|
||||
|
||||
.rio-media-player video {
|
||||
pointer-events: none;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
@include kill-size-request();
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
@@ -1431,6 +1492,13 @@ textarea:not(:placeholder-shown) ~ .rio-input-box-label,
|
||||
pointer-events: auto;
|
||||
|
||||
scroll-behavior: smooth;
|
||||
|
||||
& > * {
|
||||
min-width: 100%;
|
||||
min-height: 100%;
|
||||
|
||||
@include single-container();
|
||||
}
|
||||
}
|
||||
|
||||
// ScrollTarget
|
||||
@@ -1651,6 +1719,8 @@ textarea:not(:placeholder-shown) ~ .rio-input-box-label,
|
||||
.rio-drawer-anchor {
|
||||
pointer-events: none;
|
||||
|
||||
@include single-container();
|
||||
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
@@ -2000,7 +2070,6 @@ textarea:not(:placeholder-shown) ~ .rio-input-box-label,
|
||||
top: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
z-index: $z-index-error-popup;
|
||||
|
||||
background-color: transparent;
|
||||
|
||||
|
||||
@@ -39,7 +39,6 @@ from .progress_bar import *
|
||||
from .progress_circle import *
|
||||
from .rectangle import *
|
||||
from .revealer import *
|
||||
from .scroll_container import *
|
||||
from .scroll_target import *
|
||||
from .separator import *
|
||||
from .slider import *
|
||||
|
||||
@@ -218,6 +218,9 @@ class Component(abc.ABC, metaclass=ComponentMeta):
|
||||
align_x: float | None = None
|
||||
align_y: float | None = None
|
||||
|
||||
scroll_x: Literal["never", "auto", "always"] = "never"
|
||||
scroll_y: Literal["never", "auto", "always"] = "never"
|
||||
|
||||
margin_left: float | None = None
|
||||
margin_top: float | None = None
|
||||
margin_right: float | None = None
|
||||
|
||||
@@ -6,7 +6,6 @@ from typing import * # type: ignore
|
||||
from .. import utils
|
||||
from .component import Component
|
||||
from .fundamental_component import FundamentalComponent
|
||||
from .scroll_container import ScrollContainer
|
||||
|
||||
__all__ = ["HighLevelRootComponent"]
|
||||
|
||||
@@ -53,17 +52,12 @@ class HighLevelRootComponent(Component):
|
||||
# Build the user's root component
|
||||
user_root = utils.safe_build(self.build_function)
|
||||
|
||||
# Wrap it up in a scroll container, so the dev-tools don't scroll
|
||||
user_content = ScrollContainer(
|
||||
user_root,
|
||||
scroll_x=scroll,
|
||||
scroll_y=scroll,
|
||||
)
|
||||
|
||||
return FundamentalRootComponent(
|
||||
user_content,
|
||||
user_root,
|
||||
utils.safe_build(self.build_connection_lost_message_function),
|
||||
dev_tools=dev_tools,
|
||||
scroll_x=scroll,
|
||||
scroll_y=scroll,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import KW_ONLY
|
||||
from typing import Literal, final
|
||||
|
||||
import rio
|
||||
|
||||
from .fundamental_component import FundamentalComponent
|
||||
|
||||
__all__ = ["ScrollContainer"]
|
||||
|
||||
|
||||
@final
|
||||
class ScrollContainer(FundamentalComponent):
|
||||
"""
|
||||
Displays a scroll bar if its content grows too large.
|
||||
|
||||
`ScrollContainer` is a container which can display child components that
|
||||
would normally be too large to fit on the screen. It accepts and displays a
|
||||
single child and adds scroll bars if smaller than the child. If you can also
|
||||
force enable or disable scrollbars using the `scroll_x` and `scroll_y`
|
||||
properties.
|
||||
|
||||
|
||||
## Attributes
|
||||
|
||||
`content`: The child component to display inside the `ScrollContainer`.
|
||||
|
||||
`scroll_x`: Controls horizontal scrolling. The default is `"auto"`, which
|
||||
means that a scroll bar will be displayed only if it is needed.
|
||||
`"always"` displays a scroll bar even if it isn't needed, and
|
||||
`"never"` disables horizontal scrolling altogether.
|
||||
|
||||
`scroll_y`: Controls vertical scrolling. The default is `"auto"`, which
|
||||
means that a scroll bar will be displayed only if it is needed.
|
||||
`"always"` displays a scroll bar even if it isn't needed, and
|
||||
`"never"` disables vertical scrolling altogether.
|
||||
|
||||
`initial_x`: The initial location of the scroll knob along the horizontal
|
||||
axis. Ranges from 0 (left) to 1 (right).
|
||||
|
||||
`initial_y`: The initial location of the scroll knob along the vertical
|
||||
axis. Ranges from 0 (top) to 1 (bottom).
|
||||
|
||||
`sticky_bottom`: If `True`, when the user has scrolled to the bottom and
|
||||
the content of the `ScrollContainer` grows larger, the scroll bar
|
||||
will automatically scroll to the bottom again.
|
||||
|
||||
|
||||
## Examples
|
||||
|
||||
A minimal example of `ScrollContainer` displaying an icon:
|
||||
|
||||
```python
|
||||
rio.ScrollContainer(
|
||||
content=rio.Icon("material/castle", width=50, height=50),
|
||||
height=10,
|
||||
)
|
||||
```
|
||||
"""
|
||||
|
||||
content: rio.Component
|
||||
_: KW_ONLY
|
||||
scroll_x: Literal["never", "auto", "always"] = "auto"
|
||||
scroll_y: Literal["never", "auto", "always"] = "auto"
|
||||
initial_x: float = 0
|
||||
initial_y: float = 0
|
||||
sticky_bottom: bool = False
|
||||
|
||||
|
||||
ScrollContainer._unique_id = "ScrollContainer-builtin"
|
||||
@@ -79,6 +79,7 @@ def serialize_and_host_component(component: rio.Component) -> JsonDoc:
|
||||
"_python_type_": type(component).__name__,
|
||||
"_key_": component.key,
|
||||
"_rio_internal_": component._rio_internal_,
|
||||
"_scroll_": [component.scroll_x, component.scroll_y],
|
||||
}
|
||||
|
||||
# Accessing state properties is pretty slow, so we'll store these in local
|
||||
@@ -149,6 +150,9 @@ def get_attribute_serializers(
|
||||
Returns a dictionary of attribute names to their types that should be
|
||||
serialized for the given component class.
|
||||
"""
|
||||
if cls is rio.Component:
|
||||
return {}
|
||||
|
||||
serializers: dict[str, Serializer] = {}
|
||||
|
||||
for base_cls in reversed(cls.__bases__):
|
||||
|
||||
Reference in New Issue
Block a user