diff --git a/frontend/code/componentManagement.ts b/frontend/code/componentManagement.ts index f9c0c7b7..bf4893d8 100644 --- a/frontend/code/componentManagement.ts +++ b/frontend/code/componentManagement.ts @@ -1,3 +1,4 @@ +import { AspectRatioContainerComponent } from './components/aspectRatioContainer'; import { BuildFailedComponent } from './components/buildFailed'; import { ButtonComponent, IconButtonComponent } from './components/buttons'; import { CalendarComponent } from './components/calendar'; @@ -10,8 +11,10 @@ import { ColorPickerComponent } from './components/colorPicker'; import { ColumnComponent, RowComponent } from './components/linearContainers'; import { ComponentBase, ComponentState } from './components/componentBase'; import { ComponentId } from './dataModels'; +import { ComponentPickerComponent } from './components/componentPicker'; import { ComponentTreeComponent } from './components/componentTree'; import { CustomListItemComponent } from './components/customListItem'; +import { devToolsConnector } from './app'; import { DevToolsConnectorComponent } from './components/devToolsConnector'; import { DrawerComponent } from './components/drawer'; import { DropdownComponent } from './components/dropdown'; @@ -19,6 +22,7 @@ import { FlowComponent as FlowContainerComponent } from './components/flowContai import { FundamentalRootComponent } from './components/fundamentalRootComponent'; import { GridComponent } from './components/grid'; import { HeadingListItemComponent } from './components/headingListItem'; +import { HighLevelComponent as HighLevelComponent } from './components/highLevelComponent'; import { HtmlComponent } from './components/html'; import { IconComponent } from './components/icon'; import { ImageComponent } from './components/image'; @@ -33,7 +37,6 @@ import { MultiLineTextInputComponent } from './components/multiLineTextInput'; import { NodeInputComponent } from './components/nodeInput'; import { NodeOutputComponent } from './components/nodeOutput'; import { OverlayComponent } from './components/overlay'; -import { HighLevelComponent as HighLevelComponent } from './components/highLevelComponent'; import { PlotComponent } from './components/plot'; import { PopupComponent } from './components/popup'; import { ProgressBarComponent } from './components/progressBar'; @@ -41,6 +44,7 @@ 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'; @@ -55,11 +59,9 @@ import { TextComponent } from './components/text'; import { TextInputComponent } from './components/textInput'; import { ThemeContextSwitcherComponent } from './components/themeContextSwitcher'; import { TooltipComponent } from './components/tooltip'; -import { devToolsConnector } from './app'; -import { ComponentPickerComponent } from './components/componentPicker'; -import { ScrollContainerComponent } from './components/scrollContainer'; const COMPONENT_CLASSES = { + 'AspectRatioContainer-builtin': AspectRatioContainerComponent, 'BuildFailed-builtin': BuildFailedComponent, 'Button-builtin': ButtonComponent, 'Calendar-builtin': CalendarComponent, diff --git a/frontend/code/components/aspectRatioContainer.ts b/frontend/code/components/aspectRatioContainer.ts new file mode 100644 index 00000000..7acfa4e5 --- /dev/null +++ b/frontend/code/components/aspectRatioContainer.ts @@ -0,0 +1,103 @@ +import { ComponentBase, ComponentState } from './componentBase'; +import { ComponentId } from '../dataModels'; +import { getNaturalSizeInPixels } from '../utils'; + +export type AspectRatioContainerState = ComponentState & { + _type_: 'AspectRatioContainer-builtin'; + content?: ComponentId; + aspect_ratio: number; +}; + +export class AspectRatioContainerComponent extends ComponentBase { + state: Required; + + private innerElement: HTMLElement; + private childContainer: HTMLElement; + + private parentResizeObserver: ResizeObserver; + private childResizeObserver: ResizeObserver; + + createElement(): HTMLElement { + let element = document.createElement('div'); + element.classList.add('rio-aspect-ratio-container'); + + // Add a second element to apply the child's size to + this.innerElement = document.createElement('div'); + element.appendChild(this.innerElement); + + // Add a child container + this.childContainer = document.createElement('div'); + this.childContainer.classList.add( + 'rio-aspect-ratio-container-child-container' + ); + this.innerElement.appendChild(this.childContainer); + + // Listen for changes + this.parentResizeObserver = new ResizeObserver( + this.onParentResize.bind(this) + ); + this.parentResizeObserver.observe(element); + + this.childResizeObserver = new ResizeObserver( + this.onParentResize.bind(this) + ); + this.childResizeObserver.observe(this.childContainer); + + return element; + } + + onDestruction(): void { + this.parentResizeObserver.disconnect(); + this.childResizeObserver.disconnect(); + } + + updateElement( + deltaState: AspectRatioContainerState, + latentComponents: Set + ): void { + super.updateElement(deltaState, latentComponents); + + if (deltaState.content !== undefined) { + this.replaceOnlyChild( + latentComponents, + deltaState.content, + this.childContainer + ); + } + + if (deltaState.aspect_ratio !== undefined) { + this.childContainer.style.aspectRatio = + deltaState.aspect_ratio.toString(); + } + } + + onParentResize(): void { + // Get the parent's and child's dimensions + let parentRect = this.element.getBoundingClientRect(); + let parentAspectRatio = parentRect.width / parentRect.height; + + // Update the child's dimensions + if (parentAspectRatio > this.state.aspect_ratio) { + this.childContainer.style.width = 'auto'; + this.childContainer.style.height = '100%'; + + this.childContainer.style.left = '50%'; + this.childContainer.style.top = '0'; + this.childContainer.style.transform = 'translateX(-50%)'; + } else { + this.childContainer.style.width = '100%'; + this.childContainer.style.height = 'auto'; + + this.childContainer.style.left = '0'; + this.childContainer.style.top = '50%'; + this.childContainer.style.transform = 'translateY(-50%)'; + } + } + + onChildResize(): void { + let childElement = this.innerElement.firstElementChild as HTMLElement; + let childNaturalSize = getNaturalSizeInPixels(childElement); + this.innerElement.style.minWidth = `${childNaturalSize[0]}px`; + this.innerElement.style.minHeight = `${childNaturalSize[1]}px`; + } +} diff --git a/frontend/css/style.scss b/frontend/css/style.scss index 1e35ce1e..dde7cbb5 100644 --- a/frontend/css/style.scss +++ b/frontend/css/style.scss @@ -3628,3 +3628,13 @@ html.picking-component * { .rio-checkbox.is-on .rio-checkbox-check { transform: scale(1); } + +// Aspect Ratio Container +.rio-aspect-ratio-container { + @include single-container(); + + & > div > .rio-aspect-ratio-container-child-container { + position: absolute; + @include single-container(); + } +} diff --git a/rio/components/__init__.py b/rio/components/__init__.py index 75f8907d..bbcdec2b 100644 --- a/rio/components/__init__.py +++ b/rio/components/__init__.py @@ -1,3 +1,4 @@ +from .aspect_ratio_container import * from .auto_form import * from .banner import * from .button import * diff --git a/rio/components/aspect_ratio_container.py b/rio/components/aspect_ratio_container.py new file mode 100644 index 00000000..b70bd240 --- /dev/null +++ b/rio/components/aspect_ratio_container.py @@ -0,0 +1,19 @@ +from __future__ import annotations + +import rio + +from .fundamental_component import FundamentalComponent + +__all__ = [ + "AspectRatioContainer", +] + + +class AspectRatioContainer(FundamentalComponent): + # TODO + + content: rio.Component + aspect_ratio: float + + +AspectRatioContainer._unique_id = "AspectRatioContainer-builtin"