diff --git a/frontend/code/components/switcherBar.ts b/frontend/code/components/switcherBar.ts index 5508cdfc..03410c88 100644 --- a/frontend/code/components/switcherBar.ts +++ b/frontend/code/components/switcherBar.ts @@ -1,25 +1,7 @@ import { ComponentBase, ComponentState } from './componentBase'; import { ColorSet } from '../dataModels'; import { applySwitcheroo } from '../designApplication'; -import { easeInOut } from '../easeFunctions'; -import { firstDefined } from '../utils'; - -const ACCELERATION: number = 350; // rem/s^2 - -const MARKER_FADE_DURATION: number = 0.18; // s - -// Whitespace around each option -const OPTION_MARGIN: number = 0.5; - -// Width & height of the SVG in each option -const ICON_HEIGHT: number = 1.8; - -// Whitespace between the icon and the text, if both are present -const ICON_MARGIN: number = 0.5; - -const TEXT_STYLE_CSS_OPTIONS: object = { - 'font-weight': 'bold', -}; +import { firstDefined, zip } from '../utils'; export type SwitcherBarState = ComponentState & { _type_: 'SwitcherBar-builtin'; @@ -35,385 +17,26 @@ export type SwitcherBarState = ComponentState & { export class SwitcherBarComponent extends ComponentBase { state: Required; - private innerElement: HTMLElement; + private optionsContainer: HTMLElement; private markerElement: HTMLElement; - private backgroundOptionsElement: HTMLElement; - private markerOptionsElement: HTMLElement; - - // The width and height of each option. Icon, string and all - private optionWidths: number[]; - private optionHeights: number[]; - - // Marker animation state - private markerCurFade: number; - - private markerCurLeft: number = 0; - private markerCurTop: number = 0; - private markerCurWidth: number = 0; - private markerCurHeight: number = 0; - - private markerCurVelocity: number = 0; - - // -1 if no animation is running - private lastAnimationTickAt: number = -1; - - // Allows to determine whether this is the first time the element is being - // updated. - private isInitialized: boolean = false; createElement(): HTMLElement { // Create the elements - let elementOuter = document.createElement('div'); - elementOuter.classList.add('rio-switcher-bar'); - - // Centers the bar - this.innerElement = document.createElement('div'); - elementOuter.appendChild(this.innerElement); + let element = document.createElement('div'); + element.classList.add('rio-switcher-bar'); // Highlights the selected item this.markerElement = document.createElement('div'); this.markerElement.classList.add('rio-switcher-bar-marker'); + element.appendChild(this.markerElement); - return elementOuter; - } - - /// Instantly move the marker to the position stored in the instance - placeMarkerToState(): void { - // Account for the marker's resize animation - let easedFade = easeInOut(this.markerCurFade); - let scaledWidth = this.markerCurWidth * easedFade; - let scaledHeight = this.markerCurHeight * easedFade; - - let left = this.markerCurLeft + (this.markerCurWidth - scaledWidth) / 2; - let top = this.markerCurTop + (this.markerCurHeight - scaledHeight) / 2; - - // Move the marker - this.markerElement.style.left = `${left}rem`; - this.markerElement.style.top = `${top}rem`; - this.markerElement.style.width = `${scaledWidth}rem`; - this.markerElement.style.height = `${scaledHeight}rem`; - - // The inner options are positioned relative to the marker. Move them in - // the opposite direction so they stay put. - this.markerOptionsElement.style.left = `-${left}rem`; - this.markerOptionsElement.style.top = `-${top}rem`; - } - - /// If an item is selected, returns the position and size the marker should - /// be in order to highlight the selected item. Returns `null` if no item is - /// currently selected. - getMarkerTarget(): [number, number, number, number] | null { - // Nothing selected - if (this.state.selectedName === null) { - return null; - } - - // Find the selected item - let selectedIndex = this.state.names.indexOf(this.state.selectedName!); - console.assert(selectedIndex !== -1); - - // Find the location of the selected item. This could be done using - // `getBoundingClientRect`, but for some reason that seems to yield - // wrong results occasionally. - - // Horizontal - if (this.state.orientation == 'horizontal') { - let additionalWidth = this.allocatedWidth - this.naturalWidth; - let left = 0; - - // Spacing - if (this.state.names.length === 1) { - left = additionalWidth / 2; - } else { - let spacing = - additionalWidth / (this.state.names.length - 1) + - this.state.spacing; - left = spacing * selectedIndex; - } - - // Items - for (let i = 0; i < selectedIndex; i++) { - left += this.optionWidths[i]; - } - - // Combine everything - return [ - left, - 0, - this.optionWidths[selectedIndex], - this.naturalHeight, - ]; - } - - // Vertical - else { - let additionalHeight = this.allocatedHeight - this.naturalHeight; - let top = 0; - - // Spacing - if (this.state.names.length === 1) { - top = additionalHeight / 2; - } else { - let spacing = - additionalHeight / (this.state.names.length - 1) + - this.state.spacing; - top = spacing * selectedIndex; - } - - // Items - for (let i = 0; i < selectedIndex; i++) { - top += this.optionHeights[i]; - } - - // Combine everything - return [ - 0, - top, - this.naturalWidth, - this.optionHeights[selectedIndex], - ]; - } - } - - /// Instantly move the marker to the currently selected item - moveMarkerInstantlyIfAnimationIsntRunning(): void { - // Already transitioning? - if (this.lastAnimationTickAt !== -1) { - return; - } - - // Where to move to? - let target = this.getMarkerTarget(); - - // No target - if (target === null) { - return; - } - - // Teleport - this.markerCurLeft = target[0]; - this.markerCurTop = target[1]; - this.markerCurWidth = target[2]; - this.markerCurHeight = target[3]; - this.placeMarkerToState(); - } - - moveAnimationWorker(deltaTime: number): boolean { - // Where to move to? - let target = this.getMarkerTarget(); - - // The target may disappear while the animation is running. Handle that - // gracefully. - if (target === null) { - return false; - } - - // Calculate the distance to the target - let curPos: number, targetPos: number; - if (this.state.orientation == 'horizontal') { - curPos = this.markerCurLeft; - targetPos = target[0]; - } else { - curPos = this.markerCurTop; - targetPos = target[1]; - } - - let signedRemainingDistance = targetPos - curPos; - - // Which direction to accelerate towards? - let accelerationFactor; // + means towards the target - let brakingDistance = - Math.pow(this.markerCurVelocity, 2) / (2 * ACCELERATION); - - // Case: Moving away from the target - if ( - Math.sign(signedRemainingDistance) != - Math.sign(this.markerCurVelocity) - ) { - accelerationFactor = 3; - } - // Case: Don't run over the target quite so hard - else if (Math.abs(signedRemainingDistance) < brakingDistance) { - accelerationFactor = -1; - } - // Case: Accelerate towards the target - else { - accelerationFactor = 1; - } - - let currentAcceleration = - ACCELERATION * - accelerationFactor * - Math.sign(signedRemainingDistance); - - // Update the velocity - this.markerCurVelocity += currentAcceleration * deltaTime; - let deltaDistance = this.markerCurVelocity * deltaTime; - - // Arrived? - let t; - if (Math.abs(deltaDistance) >= Math.abs(signedRemainingDistance)) { - t = 1; - } else { - t = deltaDistance / signedRemainingDistance; - } - - // Update the marker - this.markerCurLeft += t * (target[0] - this.markerCurLeft); - this.markerCurTop += t * (target[1] - this.markerCurTop); - this.markerCurWidth += t * (target[2] - this.markerCurWidth); - this.markerCurHeight += t * (target[3] - this.markerCurHeight); - - // Done? - return t !== 1; - } - - fadeAnimationWorker(deltaTime: number): boolean { - // Fade in or out? - let target = this.state.selectedName === null ? 0 : 1; - - // Update state - let amount = - (Math.sign(target - this.markerCurFade) * deltaTime) / - MARKER_FADE_DURATION; - this.markerCurFade += amount; - this.markerCurFade = Math.min(Math.max(this.markerCurFade, 0), 1); - - // Keep going? - return this.markerCurFade !== target; - } - - animationWorker() { - // How much time has passed? - let now = Date.now(); - let deltaTime = (now - this.lastAnimationTickAt) / 1000; - this.lastAnimationTickAt = now; - - // Run the animations - let moveKeepGoing = this.moveAnimationWorker(deltaTime); - let fadeKeepGoing = this.fadeAnimationWorker(deltaTime); - let keepGoing = moveKeepGoing || fadeKeepGoing; - - // Update the marker to match the current state - this.placeMarkerToState(); - - // Keep going? - if (keepGoing) { - requestAnimationFrame(this.animationWorker.bind(this)); - } else { - this.lastAnimationTickAt = -1; - } - } - - startAnimationIfNotRunning(): void { - // Already running? - if (this.lastAnimationTickAt !== -1) { - return; - } - - // Nope, get going - this.lastAnimationTickAt = Date.now(); - this.markerCurVelocity = 0; - this.animationWorker(); - } - - /// High level function to update the marker. It will animate the marker as - /// appropriate. - switchMarkerToSelectedName(): void { - // No value selected? Fade out - let target = this.getMarkerTarget(); - if (target === null) { - this.startAnimationIfNotRunning(); - return; - } - - // If the marker is currently invisible, teleport it - if (this.markerCurFade === 0) { - this.markerCurLeft = target[0]; - this.markerCurTop = target[1]; - this.markerCurWidth = target[2]; - this.markerCurHeight = target[3]; - this.placeMarkerToState(); - } - - // Start the animation(s) - this.startAnimationIfNotRunning(); - } - - buildContent(deltaState: SwitcherBarState): HTMLElement { - let result = document.createElement('div'); - result.classList.add('rio-switcher-bar-options'); - Object.assign(result.style, TEXT_STYLE_CSS_OPTIONS); - result.style.removeProperty('color'); - - let names = firstDefined(deltaState.names, this.state.names); - let iconSvgSources = firstDefined( - deltaState.icon_svg_sources, - this.state.icon_svg_sources + this.optionsContainer = document.createElement('div'); + this.optionsContainer.classList.add( + 'rio-switcher-bar-options-container' ); + element.appendChild(this.optionsContainer); - // Iterate over both - for (let i = 0; i < names.length; i++) { - let name = names[i]; - let iconSvg = iconSvgSources[i]; - - let optionElement = document.createElement('div'); - optionElement.classList.add('rio-switcher-bar-option'); - optionElement.style.padding = `${OPTION_MARGIN}rem`; - result.appendChild(optionElement); - - // Icon - let iconElement; - if (iconSvg !== null) { - optionElement.innerHTML = iconSvg; - iconElement = optionElement.children[0] as HTMLElement; - iconElement.style.width = `${ICON_HEIGHT}rem`; - iconElement.style.height = `${ICON_HEIGHT}rem`; - iconElement.style.marginBottom = `${ICON_MARGIN}rem`; - iconElement.style.fill = 'currentColor'; - } - - // Text - let textElement = document.createElement('div'); - optionElement.appendChild(textElement); - textElement.textContent = name; - - // Detect clicks - optionElement.addEventListener('click', (event) => { - // If this item was already selected, the new value may be `None` - if (this.state.selectedName === name) { - if (this.state.allow_none) { - this.state.selectedName = null; - } else { - return; - } - } else { - this.state.selectedName = name; - } - - // Update the marker - this.switchMarkerToSelectedName(); - - // Notify the backend - this.sendMessageToBackend({ - name: this.state.selectedName, - }); - - // Eat the event - event.stopPropagation(); - }); - } - - // Pass the allocated size on to the content. This can't rely on CSS, - // because the inner content is necessarily located inside of the - // marker, which in turn is smaller than the full space. - if (this.state.orientation == 'horizontal') { - result.style.width = `${this.allocatedWidth}rem`; - } else { - result.style.height = `${this.allocatedHeight}rem`; - } - - return result; + return element; } updateElement( @@ -422,70 +45,15 @@ export class SwitcherBarComponent extends ComponentBase { ): void { super.updateElement(deltaState, latentComponents); - let markerPositionNeedsUpdate = false; - // Have the options changed? if ( deltaState.names !== undefined || deltaState.icon_svg_sources !== undefined ) { - { - // The option sizes need to be recomputed - this.optionWidths = []; - this.optionHeights = []; - - let names = firstDefined(deltaState.names, this.state.names); - let iconSvgSources = firstDefined( - deltaState.icon_svg_sources, - this.state.icon_svg_sources - ); - - // Iterate over both - for (let i = 0; i < names.length; i++) { - let name = names[i]; - let iconSvg = iconSvgSources[i]; - - // Text - let [width, height] = getTextDimensionsWithCss( - name, - TEXT_STYLE_CSS_OPTIONS - ); - - // Icon + margin, if present - if (iconSvg !== null) { - width = Math.max(width, ICON_HEIGHT); - height += ICON_HEIGHT + ICON_MARGIN; - } - - // Margin around - width += 2 * OPTION_MARGIN; - height += 2 * OPTION_MARGIN; - - // Store the result - this.optionWidths.push(width); - this.optionHeights.push(height); - } - } - - // The HTML needs to be rebuilt - { - this.markerElement.innerHTML = ''; - this.innerElement.innerHTML = ''; - - // Background options - this.backgroundOptionsElement = this.buildContent(deltaState); - this.innerElement.appendChild(this.backgroundOptionsElement); - - // Marker - this.innerElement.appendChild(this.markerElement); - - // Marker options - this.markerOptionsElement = this.buildContent(deltaState); - this.markerElement.appendChild(this.markerOptionsElement); - } - - // Request updates - markerPositionNeedsUpdate = true; + this.rebuildOptions( + deltaState.names ?? this.state.names, + deltaState.icon_svg_sources ?? this.state.icon_svg_sources + ); } // Color @@ -501,38 +69,104 @@ export class SwitcherBarComponent extends ComponentBase { let flexDirection = deltaState.orientation == 'vertical' ? 'column' : 'row'; - this.element.style.flexDirection = flexDirection; - this.backgroundOptionsElement.style.flexDirection = flexDirection; - this.markerOptionsElement.style.flexDirection = flexDirection; - - // Request updates - markerPositionNeedsUpdate = true; + this.optionsContainer.style.flexDirection = flexDirection; } // Spacing if (deltaState.spacing !== undefined) { - markerPositionNeedsUpdate = true; + this.optionsContainer.style.gap = `${deltaState.spacing}rem`; } // If the selection has changed make sure to move the marker if (deltaState.selectedName !== undefined) { - if (this.isInitialized) { - this.state.selectedName = deltaState.selectedName; - this.switchMarkerToSelectedName(); + if (deltaState.selectedName === null) { + this.moveMarkerTo(null); } else { - markerPositionNeedsUpdate = true; - this.markerCurFade = deltaState.selectedName === null ? 0 : 1; + let i = (deltaState.names ?? this.state.names).indexOf( + deltaState.selectedName + ); + this.moveMarkerTo(i); } } + } - // Any future updates are not the first - this.isInitialized = true; + private rebuildOptions( + names: string[], + iconSvgSources: (string | null)[] + ): void { + this.optionsContainer.innerHTML = ''; - // Perform any requested updates - Object.assign(this.state, deltaState); + for (let [index, name] of names.entries()) { + let iconSvg = iconSvgSources[index]; - if (markerPositionNeedsUpdate) { - this.moveMarkerInstantlyIfAnimationIsntRunning(); + let optionElement = this.buildOptionElement(index, name, iconSvg); + this.optionsContainer.appendChild(optionElement); } } + + private buildOptionElement( + index: number, + name: string, + iconSvg: string | null + ): HTMLElement { + let optionElement = document.createElement('div'); + optionElement.classList.add('rio-switcher-bar-option'); + + // Icon + if (iconSvg !== null) { + optionElement.innerHTML = iconSvg; + } + + // Text + let textElement = document.createElement('div'); + optionElement.appendChild(textElement); + textElement.textContent = name; + + // Detect clicks + optionElement.addEventListener('click', (event) => { + // If this item was already selected, the new value may be `None` + if (this.state.selectedName === name) { + if (this.state.allow_none) { + this.state.selectedName = null; + this.moveMarkerTo(null); + } else { + return; + } + } else { + this.state.selectedName = name; + this.moveMarkerTo(index); + } + + // Notify the backend + this.sendMessageToBackend({ + name: this.state.selectedName, + }); + + // Eat the event + event.stopPropagation(); + }); + + return optionElement; + } + + private moveMarkerTo(index: number | null): void { + if (index === null) { + this.markerElement.style.width = '0'; + this.markerElement.style.height = '0'; + return; + } + + let optionElement = this.optionsContainer.children[index]; + let optionRect = optionElement.getBoundingClientRect(); + let containerRect = this.optionsContainer.getBoundingClientRect(); + + this.markerElement.style.left = `${ + optionRect.left - containerRect.left + }px`; + this.markerElement.style.top = `${ + optionRect.top - containerRect.top + }px`; + this.markerElement.style.width = `${optionRect.width}px`; + this.markerElement.style.height = `${optionRect.height}px`; + } } diff --git a/frontend/code/devToolsTreeWalk.ts b/frontend/code/devToolsTreeWalk.ts index 861992ee..b266e2cc 100644 --- a/frontend/code/devToolsTreeWalk.ts +++ b/frontend/code/devToolsTreeWalk.ts @@ -42,37 +42,11 @@ export function getDisplayableChildren(comp: ComponentBase): ComponentBase[] { return result; } -/// Given an injected layout component, return its direct child (Which may -/// also be injected!). -export function getDirectChildOfInjectedComponent( - comp: ComponentBase -): ComponentBase { - console.assert(comp.isInjectedLayoutComponent()); - - // Both Margins and Aligns have a single child, accessible as `content` - console.assert( - comp.state._type_ === 'Margin-builtin' || - comp.state._type_ === 'Align-builtin' - ); - - let resultId: number = (comp.state as any).content; - console.assert(resultId !== undefined); - - // Return the component, not ID - return componentsById[resultId]; -} - /// Return the root component, but take care to discard any rio internal /// components. export function getDisplayedRootComponent(): ComponentBase { let rootScroller = getRootScroller(); let result = componentsById[rootScroller.state.content]!; - // This might be the user's root, but could also be injected. Keep - // digging. - while (result.isInjectedLayoutComponent()) { - result = getDirectChildOfInjectedComponent(result); - } - return result; } diff --git a/frontend/css/style.scss b/frontend/css/style.scss index c38e5075..c4dc3376 100644 --- a/frontend/css/style.scss +++ b/frontend/css/style.scss @@ -186,6 +186,8 @@ select { .rio-fundamental-root-component { display: inline-grid; + grid-template-columns: minmax(min-content, 1fr) min-content; + // The user's root component & > *:first-child { z-index: $z-index-user-root; @@ -278,6 +280,8 @@ select { pointer-events: auto; border-style: solid; + @include single-container; + overflow: hidden; // Needed by the ripple // The transition time is set via JS @@ -1485,21 +1489,6 @@ textarea:not(:placeholder-shown) ~ .rio-input-box-label, justify-content: center; } -// ScrollContainer -.rio-scroll-container { - // Needs pointer events so that the scrollbar can be interacted with - pointer-events: auto; - - scroll-behavior: smooth; - - & > * { - min-width: 100%; - min-height: 100%; - - @include single-container(); - } -} - // ScrollTarget .rio-scroll-target { pointer-events: auto; @@ -1910,33 +1899,29 @@ textarea:not(:placeholder-shown) ~ .rio-input-box-label, // SwitcherBar .rio-switcher-bar { - display: flex; - align-items: center; + @include single-container; } -.rio-switcher-bar > div { - position: relative; -} - -.rio-switcher-bar > div > .rio-switcher-bar-options { - position: relative !important; -} - -.rio-switcher-bar-options { - pointer-events: auto; - position: absolute; +.rio-switcher-bar-options-container { display: flex; align-items: stretch; justify-content: space-between; + + position: relative; // To be above the marker + + font-weight: bold; } .rio-switcher-bar-option { + pointer-events: auto; + cursor: pointer; + display: flex; flex-direction: column; align-items: center; justify-content: space-between; - cursor: pointer; + margin: 0.5rem; border-radius: var(--rio-global-corner-radius-large); color: var(--rio-local-text-color); @@ -1948,10 +1933,11 @@ textarea:not(:placeholder-shown) ~ .rio-input-box-label, background-color: var(--rio-local-bg-active); } -.rio-switcher-bar-marker - > .rio-switcher-bar-options - > .rio-switcher-bar-option { - background-color: unset !important; +.rio-switcher-bar-option > svg { + width: 1.8rem; + height: 1.8rem; + margin-bottom: 0.5rem; + fill: currentColor; } .rio-switcher-bar-option > div { @@ -1963,11 +1949,8 @@ textarea:not(:placeholder-shown) ~ .rio-input-box-label, .rio-switcher-bar-marker { pointer-events: none; background: var(--rio-local-bg); - overflow: hidden; position: absolute; - left: 0; - top: 0; border-radius: var(--rio-global-corner-radius-large); } @@ -2471,6 +2454,7 @@ textarea:not(:placeholder-shown) ~ .rio-input-box-label, // Switcher .rio-switcher { overflow: hidden; + display: none; } .rio-switcher > * { diff --git a/rio/components/root_components.py b/rio/components/root_components.py index f9f5567d..8d1b4e52 100644 --- a/rio/components/root_components.py +++ b/rio/components/root_components.py @@ -5,6 +5,7 @@ from typing import * # type: ignore from .. import utils from .component import Component +from .container import Container from .fundamental_component import FundamentalComponent __all__ = ["HighLevelRootComponent"] @@ -33,11 +34,11 @@ class HighLevelRootComponent(Component): # The browser handles scrolling automatically if the content grows too # large for the window, but when the dev tools are visible, they would # appear between the scroll bar and the scrolling content. To prevent - # this, we'll wrap the user content in a ScrollContainer. + # this, we'll wrap the user content in a scrolling container. # - # Conditionally inserting this ScrollContainer would make a bunch of - # code more messy, so we'll *always* insert the ScrollContainer but - # conditionally disable it with `scroll='never'`. + # Conditionally inserting this scrolling container would make a bunch of + # code more messy, so we'll *always* insert the container but only + # enable scrolling if necessary. if self.session._app_server.debug_mode: # Avoid a circular import import rio.debug.dev_tools @@ -53,11 +54,13 @@ class HighLevelRootComponent(Component): user_root = utils.safe_build(self.build_function) return FundamentalRootComponent( - user_root, + Container( + user_root, + scroll_x=scroll, + scroll_y=scroll, + ), utils.safe_build(self.build_connection_lost_message_function), dev_tools=dev_tools, - scroll_x=scroll, - scroll_y=scroll, ) diff --git a/rio/debug/dev_tools/icons_page.py b/rio/debug/dev_tools/icons_page.py index 72583576..21f88469 100644 --- a/rio/debug/dev_tools/icons_page.py +++ b/rio/debug/dev_tools/icons_page.py @@ -426,11 +426,9 @@ Use the `rio.Icon` component like this: children.append(self.build_details()) # Combine everything - return rio.ScrollContainer( - rio.Column( - *children, - spacing=1, - margin=1, - ), - scroll_x="never", + return rio.Column( + *children, + spacing=1, + margin=1, + scroll_y="auto", ) diff --git a/rio/debug/dev_tools/theme_picker_page.py b/rio/debug/dev_tools/theme_picker_page.py index 4762a7c2..595724a5 100644 --- a/rio/debug/dev_tools/theme_picker_page.py +++ b/rio/debug/dev_tools/theme_picker_page.py @@ -404,121 +404,117 @@ class ThemePickerPage(rio.Component): ) # Combine everything - return rio.ScrollContainer( - content=rio.Column( - # Main Colors - PalettePicker( - shared_open_key=self.bind().shared_open_key, - palette_nicename="Primary", - palette_slug="primary", - round_top=True, - ), - PalettePicker( - shared_open_key=self.bind().shared_open_key, - palette_nicename="Secondary", - palette_slug="secondary", - round_bottom=True, - ), - # Neutral Colors - PalettePicker( - shared_open_key=self.bind().shared_open_key, - palette_nicename="Background", - palette_slug="background", - margin_top=1, - round_top=True, - ), - PalettePicker( - shared_open_key=self.bind().shared_open_key, - palette_nicename="Neutral", - palette_slug="neutral", - ), - PalettePicker( - shared_open_key=self.bind().shared_open_key, - palette_nicename="HUD", - palette_slug="hud", - pick_opacity=True, - ), - PalettePicker( - shared_open_key=self.bind().shared_open_key, - palette_nicename="Disabled", - palette_slug="disabled", - round_bottom=True, - ), - # Semantic Colors - PalettePicker( - shared_open_key=self.bind().shared_open_key, - palette_nicename="Success", - palette_slug="success", - margin_top=1, - round_top=True, - ), - PalettePicker( - shared_open_key=self.bind().shared_open_key, - palette_nicename="Warning", - palette_slug="warning", - ), - PalettePicker( - shared_open_key=self.bind().shared_open_key, - palette_nicename="Danger", - palette_slug="danger", - round_bottom=True, - ), - # Corner radii - rio.Text( - "Corner Radii", - style="heading3", - margin_top=1, - margin_bottom=1, - justify="left", - ), - radius_sliders, - # Theme Variants - rio.Text( - "Variants", - style="heading3", - margin_top=1, - margin_bottom=1, - justify="left", - ), - rio.Grid( - [ - rio.Switch( - is_on=self.create_light_theme, - on_change=self._toggle_create_light_theme, - ), - rio.Text("Light Theme"), - rio.Spacer(), - ], - [ - rio.Switch( - is_on=self.create_dark_theme, - on_change=self._toggle_create_dark_theme, - ), - rio.Text("Dark Theme"), - rio.Spacer(), - ], - row_spacing=0.5, - column_spacing=0.5, - ), - # Code Sample - rio.Text( - "Code", - style="heading3", - margin_top=1, - margin_bottom=1, - justify="left", - ), - rio.Markdown( - f""" + return rio.Column( + # Main Colors + PalettePicker( + shared_open_key=self.bind().shared_open_key, + palette_nicename="Primary", + palette_slug="primary", + round_top=True, + ), + PalettePicker( + shared_open_key=self.bind().shared_open_key, + palette_nicename="Secondary", + palette_slug="secondary", + round_bottom=True, + ), + # Neutral Colors + PalettePicker( + shared_open_key=self.bind().shared_open_key, + palette_nicename="Background", + palette_slug="background", + margin_top=1, + round_top=True, + ), + PalettePicker( + shared_open_key=self.bind().shared_open_key, + palette_nicename="Neutral", + palette_slug="neutral", + ), + PalettePicker( + shared_open_key=self.bind().shared_open_key, + palette_nicename="HUD", + palette_slug="hud", + pick_opacity=True, + ), + PalettePicker( + shared_open_key=self.bind().shared_open_key, + palette_nicename="Disabled", + palette_slug="disabled", + round_bottom=True, + ), + # Semantic Colors + PalettePicker( + shared_open_key=self.bind().shared_open_key, + palette_nicename="Success", + palette_slug="success", + margin_top=1, + round_top=True, + ), + PalettePicker( + shared_open_key=self.bind().shared_open_key, + palette_nicename="Warning", + palette_slug="warning", + ), + PalettePicker( + shared_open_key=self.bind().shared_open_key, + palette_nicename="Danger", + palette_slug="danger", + round_bottom=True, + ), + # Corner radii + rio.Text( + "Corner Radii", + style="heading3", + margin_top=1, + margin_bottom=1, + justify="left", + ), + radius_sliders, + # Theme Variants + rio.Text( + "Variants", + style="heading3", + margin_top=1, + margin_bottom=1, + justify="left", + ), + rio.Grid( + [ + rio.Text("Light Theme", justify="left"), + rio.Switch( + is_on=self.create_light_theme, + on_change=self._toggle_create_light_theme, + ), + ], + [ + rio.Text("Dark Theme", justify="left"), + rio.Switch( + is_on=self.create_dark_theme, + on_change=self._toggle_create_dark_theme, + ), + ], + row_spacing=0.5, + column_spacing=0.5, + ), + # Code Sample + rio.Text( + "Code", + style="heading3", + margin_top=1, + margin_bottom=1, + justify="left", + ), + rio.Markdown( + f""" Use this code to recreate the current theme in your app: ```python {get_source_for_theme(self.session.theme, create_theme_pair=self.create_light_theme and self.create_dark_theme)} ``` """, - ), - margin=1, - align_y=0, ), - scroll_x="never", + margin=1, + align_y=0, + scroll_y="auto", ) diff --git a/rio/debug/dev_tools/tree_page.py b/rio/debug/dev_tools/tree_page.py index 63a04902..636565c8 100644 --- a/rio/debug/dev_tools/tree_page.py +++ b/rio/debug/dev_tools/tree_page.py @@ -147,18 +147,14 @@ class TreePage(rio.Component): # Delegate the hard work to another component return rio.Column( self._build_back_menu(heading), - rio.ScrollContainer( - component_details.ComponentDetails( - component_id=self.bind().selected_component_id, - on_switch_to_layout_view=self._switch_to_layout, - margin_right=MARGIN, - align_y=0, - ), - scroll_x="never", - scroll_y="auto", + component_details.ComponentDetails( + component_id=self.bind().selected_component_id, + on_switch_to_layout_view=self._switch_to_layout, margin_left=MARGIN, + margin_right=MARGIN, margin_bottom=MARGIN, height="grow", + scroll_y="auto", ), ) @@ -168,17 +164,11 @@ class TreePage(rio.Component): return rio.Column( self._build_back_menu("Layout"), - rio.ScrollContainer( - layout_subpage.LayoutSubpage( - component_id=self.bind().selected_component_id, - margin_right=MARGIN, - align_y=0, - ), - scroll_x="never", + layout_subpage.LayoutSubpage( + component_id=self.bind().selected_component_id, + margin=MARGIN, + align_y=0, scroll_y="auto", - margin_left=MARGIN, - margin_top=MARGIN, - margin_bottom=MARGIN, height="grow", ), ) diff --git a/rio/session.py b/rio/session.py index f324a1b7..4fe5c1d6 100644 --- a/rio/session.py +++ b/rio/session.py @@ -622,9 +622,7 @@ class Session(unicall.Unicall): ), low_level_root scroll_container = low_level_root.content - assert isinstance( - scroll_container, rio.ScrollContainer - ), scroll_container + assert isinstance(scroll_container, rio.Container), scroll_container return scroll_container.content diff --git a/rio/snippets/snippet-files/project-template-AI Chatbot/pages/chat_page.py b/rio/snippets/snippet-files/project-template-AI Chatbot/pages/chat_page.py index e1fc5a0b..28eb3951 100644 --- a/rio/snippets/snippet-files/project-template-AI Chatbot/pages/chat_page.py +++ b/rio/snippets/snippet-files/project-template-AI Chatbot/pages/chat_page.py @@ -138,21 +138,18 @@ class ChatPage(rio.Component): ), rio.Column( # Messages - rio.ScrollContainer( - rio.Column( - # Display the messages - *message_components, - # Take up superfluous space - rio.Spacer(), - spacing=1, - # Center the column on wide screens - width=column_width, - margin=2, - align_x=column_align_x, - ), - scroll_x="never", - scroll_y="auto", + rio.Column( + # Display the messages + *message_components, + # Take up superfluous space + rio.Spacer(), + spacing=1, + # Center the column on wide screens + margin=2, + width=column_width, + align_x=column_align_x, height="grow", + scroll_y="auto", ), # User input rio.Row(