Merge branch 'main' into patch-1

This commit is contained in:
Mad Cow
2024-10-24 06:27:17 +02:00
committed by GitHub
258 changed files with 2967 additions and 1729 deletions

View File

@@ -1,94 +1,100 @@
# Changelog
- `rio.Dropdown` will now open a fullscreen popup on mobile devices
- `rio.MediaPlayer` now also triggers the `on_playback_end` event when the
- New styles for input boxes: "rounded" and "pill"
- Improved mobile support: Dragging is now much smoother
## 0.10
- `rio.Dropdown` will now open a fullscreen popup on mobile devices
- `rio.MediaPlayer` now also triggers the `on_playback_end` event when the
video loops
- experimental support for base-URL
- dialogs!
- dialogs can now store a result value similar to futures
- `rio.Text.wrap` is now `rio.Text.overflow`. Same for markdown.
- removed `rio.Popup.on_open_or_close`. This event never actually fired.
- `rio.Link` can now optionally display an icon
- Rio will automatically create basic navigation for you, if your app has more
- experimental support for base-URL
- dialogs!
- dialogs can now store a result value similar to futures
- `rio.Text.wrap` is now `rio.Text.overflow`. Same for markdown.
- removed `rio.Popup.on_open_or_close`. This event never actually fired.
- `rio.Link` can now optionally display an icon
- Rio will automatically create basic navigation for you, if your app has more
than one page
- Updated button styles: Added `colored-text` and renamed `plain` ->
- Updated button styles: Added `colored-text` and renamed `plain` ->
`plain-text`
- Methods for creating dialogs are now in `rio.Session` rather than
- Methods for creating dialogs are now in `rio.Session` rather than
`rio.Component`.
- Page rework
- Add `rio.Redirect`
- TODO: Automatic page scan
- Page rework
- Add `rio.Redirect`
- TODO: Automatic page scan
- New experimental `rio.FilePickerArea` component
## 0.9.2
- restyled `rio.Switch`
- New ~~experimental~~ broken component `AspectRatioContainer`
- restyled `rio.Switch`
- New ~~experimental~~ broken component `AspectRatioContainer`
## 0.9.1
- added gain_focus / lose_focus events to TextInput and NumberInput
- `.rioignore` has been superseeded by the new `project-files` setting in
- added gain_focus / lose_focus events to TextInput and NumberInput
- `.rioignore` has been superseeded by the new `project-files` setting in
`rio.toml`
- values in `rio.toml` are now written in kebab-case instead of
- values in `rio.toml` are now written in kebab-case instead of
all_lower_case. Rio will still recognize the old names and automatically fix
them for you.
- deprecated `light` parameter of `Theme.from_color`, has been superseded by
- deprecated `light` parameter of `Theme.from_color`, has been superseded by
`mode`
- Tooltips now default to `position="auto"`
- Icons now use `_` instead of `-` in their names. This brings them more in line
- Tooltips now default to `position="auto"`
- Icons now use `_` instead of `-` in their names. This brings them more in line
with Python naming conventions
- Checkbox restyling
- Checkbox restyling
## 0.9
- Buttons now have a smaller minimum size when using a `rio.Component` as
- Buttons now have a smaller minimum size when using a `rio.Component` as
content
- `FrostedGlassFill` added (Contributed by MiniTT)
- added `@rio.event.on_window_size_change`
- popups now default to the "hud" color
- popups and tooltips are no longer cut off by other components
- Add HTML meta tags
- Add functions for reading and writing clipboard contents to the `Session`
- `FrostedGlassFill` added (Contributed by MiniTT)
- added `@rio.event.on_window_size_change`
- popups now default to the "hud" color
- popups and tooltips are no longer cut off by other components
- Add HTML meta tags
- Add functions for reading and writing clipboard contents to the `Session`
(Contributed by MiniTT)
- The color of drawers is now configurable, and also sets the theme context
- added `Calendar` component
- added `DateInput` component
- massive dev-tools overhaul
- new (but experimental) `Switcher` component
- TextInputs now update their text in real-time
- `rio run` no longer opens a browser
- `rio.HTML` components now execute embedded `<script>` nodes
- added `Checkbox` Component
- `FlowContainer` now has a convenience `spacing` parameter which controls both
- The color of drawers is now configurable, and also sets the theme context
- added `Calendar` component
- added `DateInput` component
- massive dev-tools overhaul
- new (but experimental) `Switcher` component
- TextInputs now update their text in real-time
- `rio run` no longer opens a browser
- `rio.HTML` components now execute embedded `<script>` nodes
- added `Checkbox` Component
- `FlowContainer` now has a convenience `spacing` parameter which controls both
`row_spacing` and `column_spacing` at the same time
deprecations:
- `rio.Fill` and `rio.FillLike` deprecated. Most components only support
- `rio.Fill` and `rio.FillLike` deprecated. Most components only support
specific fills, so these have no purpose any more
- `display_controls` parameter of `CodeBlock` component renamed to
- `display_controls` parameter of `CodeBlock` component renamed to
`show_controls`
breaking:
- `Text.justify` now defaults to `"left"`
- `FlowContainer.justify` now defaults to `"left"`
- `rio.Theme` is no longer frozen, and can now be modified. This is breaking,
- `Text.justify` now defaults to `"left"`
- `FlowContainer.justify` now defaults to `"left"`
- `rio.Theme` is no longer frozen, and can now be modified. This is breaking,
because the `replace` method has been removed
## 0.8
- Rectangles now honor the theme's shadow color
- Renamed `Banner.markup` to `Banner.markdown`
- Removed the "multiline" style from Banners
- Removed `Button.initially_disabled_for`
- Added a `text_color` parameter to `Theme.from_colors` and
- Rectangles now honor the theme's shadow color
- Renamed `Banner.markup` to `Banner.markdown`
- Removed the "multiline" style from Banners
- Removed `Button.initially_disabled_for`
- Added a `text_color` parameter to `Theme.from_colors` and
`Theme.pair_from_colors`
- `rio run` now checks that the installed version of Rio is up-to-date
- `rio run` now checks that the installed version of Rio is up-to-date
## 0.7
- New example: multi-page website
- New component: CodeBlock
- UserSettings can now have mutable default values
- Removed "undefined space"
- New example: multi-page website
- New component: CodeBlock
- UserSettings can now have mutable default values
- Removed "undefined space"

View File

@@ -17,7 +17,7 @@ import { ComponentTreeComponent } from "./components/componentTree";
import { CustomListItemComponent } from "./components/customListItem";
import { devToolsConnector } from "./app";
import { DevToolsConnectorComponent } from "./components/devToolsConnector";
import { DialogContainerComponent } from "./components/dialog_container";
import { DialogContainerComponent } from "./components/dialogContainer";
import { DrawerComponent } from "./components/drawer";
import { DropdownComponent } from "./components/dropdown";
import { FilePickerAreaComponent } from "./components/filePickerArea";
@@ -62,10 +62,11 @@ import { TextComponent } from "./components/text";
import { TextInputComponent } from "./components/textInput";
import { ThemeContextSwitcherComponent } from "./components/themeContextSwitcher";
import { TooltipComponent } from "./components/tooltip";
import { PointerEventListenerComponent } from "./components/pointerEventListener";
import { WebsiteComponent } from "./components/website";
const COMPONENT_CLASSES = {
"AspectRatioContainer-builtin": AspectRatioContainerComponent,
"ErrorPlaceholder-builtin": ErrorPlaceholderComponent,
"Button-builtin": ButtonComponent,
"Calendar-builtin": CalendarComponent,
"Card-builtin": CardComponent,
@@ -82,6 +83,7 @@ const COMPONENT_CLASSES = {
"DialogContainer-builtin": DialogContainerComponent,
"Drawer-builtin": DrawerComponent,
"Dropdown-builtin": DropdownComponent,
"ErrorPlaceholder-builtin": ErrorPlaceholderComponent,
"FilePickerArea-builtin": FilePickerAreaComponent,
"FlowContainer-builtin": FlowContainerComponent,
"FundamentalRootComponent-builtin": FundamentalRootComponent,
@@ -104,6 +106,7 @@ const COMPONENT_CLASSES = {
"NodeOutput-builtin": NodeOutputComponent,
"Overlay-builtin": OverlayComponent,
"Plot-builtin": PlotComponent,
"PointerEventListener-builtin": PointerEventListenerComponent,
"Popup-builtin": PopupComponent,
"ProgressBar-builtin": ProgressBarComponent,
"ProgressCircle-builtin": ProgressCircleComponent,
@@ -125,6 +128,7 @@ const COMPONENT_CLASSES = {
"TextInput-builtin": TextInputComponent,
"ThemeContextSwitcher-builtin": ThemeContextSwitcherComponent,
"Tooltip-builtin": TooltipComponent,
"Website-builtin": WebsiteComponent,
};
globalThis.COMPONENT_CLASSES = COMPONENT_CLASSES;

View File

@@ -9,7 +9,7 @@ export type AspectRatioContainerState = ComponentState & {
};
export class AspectRatioContainerComponent extends ComponentBase {
state: Required<AspectRatioContainerState>;
declare state: Required<AspectRatioContainerState>;
private innerElement: HTMLElement;
private childContainer: HTMLElement;

View File

@@ -13,7 +13,7 @@ type AbstractButtonState = ComponentState & {
};
abstract class AbstractButtonComponent extends ComponentBase {
state: Required<AbstractButtonState>;
declare state: Required<AbstractButtonState>;
// This is the element with the `rio-button` class. The subclass is
// responsible for creating it (by calling `createButtonElement()`).
@@ -132,7 +132,7 @@ export type ButtonState = AbstractButtonState & {
};
export class ButtonComponent extends AbstractButtonComponent {
state: Required<ButtonState>;
declare state: Required<ButtonState>;
createElement(): HTMLElement {
this.buttonElement = this.createButtonElement();
@@ -147,7 +147,7 @@ export type IconButtonState = AbstractButtonState & {
};
export class IconButtonComponent extends AbstractButtonComponent {
state: Required<IconButtonState>;
declare state: Required<IconButtonState>;
private resizeObserver: ResizeObserver;

View File

@@ -16,7 +16,7 @@ export type CalendarState = ComponentState & {
};
export class CalendarComponent extends ComponentBase {
state: Required<CalendarState>;
declare state: Required<CalendarState>;
// Internal HTML Elements
private prevYearButton: HTMLElement;

View File

@@ -16,7 +16,7 @@ export type CardState = ComponentState & {
};
export class CardComponent extends ComponentBase {
state: Required<CardState>;
declare state: Required<CardState>;
// If this card has a ripple effect, this is the ripple instance. `null`
// otherwise.

View File

@@ -8,7 +8,7 @@ export type CheckboxState = ComponentState & {
};
export class CheckboxComponent extends ComponentBase {
state: Required<CheckboxState>;
declare state: Required<CheckboxState>;
private checkboxElement: HTMLInputElement;
private borderElement: HTMLElement;

View File

@@ -8,7 +8,7 @@ export type ClassContainerState = ComponentState & {
};
export class ClassContainerComponent extends ComponentBase {
state: Required<ClassContainerState>;
declare state: Required<ClassContainerState>;
createElement(): HTMLElement {
return document.createElement("div");

View File

@@ -127,7 +127,7 @@ export function convertDivToCodeBlock(
}
export class CodeBlockComponent extends ComponentBase {
state: Required<CodeBlockState>;
declare state: Required<CodeBlockState>;
createElement(): HTMLElement {
const element = document.createElement("div");

View File

@@ -13,7 +13,7 @@ export type CodeExplorerState = ComponentState & {
};
export class CodeExplorerComponent extends ComponentBase {
state: Required<CodeExplorerState>;
declare state: Required<CodeExplorerState>;
private sourceCodeElement: HTMLElement;
private arrowElement: HTMLElement;
@@ -47,14 +47,14 @@ export class CodeExplorerComponent extends ComponentBase {
[this.sourceCodeElement, this.arrowElement, this.buildResultElement] =
Array.from(element.children) as HTMLElement[];
// Listen for mouse events
// Listen for pointer events
this.buildResultElement.addEventListener(
"mousemove",
this.onResultMouseMove.bind(this),
"pointermove",
this.onResultPointerMove.bind(this),
{ capture: true }
);
this.buildResultElement.addEventListener("mouseleave", () => {
this.buildResultElement.addEventListener("pointerleave", () => {
this._highlightComponentByKey(null);
});
@@ -166,12 +166,12 @@ export class CodeExplorerComponent extends ComponentBase {
// Add the event listeners
((currentLineIndex) => {
singleSpan.addEventListener("mouseenter", () => {
singleSpan.addEventListener("pointerenter", () => {
this.onLineEntered(currentLineIndex);
});
})(lineIndex);
singleSpan.addEventListener("mouseleave", () => {
singleSpan.addEventListener("pointerleave", () => {
this.onLineEntered(null);
});
@@ -193,7 +193,7 @@ export class CodeExplorerComponent extends ComponentBase {
this._highlightComponentByKey(key);
}
private onResultMouseMove(event: MouseEvent): void {
private onResultPointerMove(event: PointerEvent): void {
// Find the element that's being hovered over
let curElement = event.target as HTMLElement;

View File

@@ -10,7 +10,7 @@ export type ColorPickerState = ComponentState & {
};
export class ColorPickerComponent extends ComponentBase {
state: Required<ColorPickerState>;
declare state: Required<ColorPickerState>;
private colorSquare: HTMLElement;
private squareKnob: HTMLElement;
@@ -25,8 +25,6 @@ export class ColorPickerComponent extends ComponentBase {
private selectedHsv: [number, number, number] = [0, 0, 0];
private latentEventHandlers: any[] = [];
private isInitialized = false;
createElement(): HTMLElement {
@@ -84,20 +82,28 @@ export class ColorPickerComponent extends ComponentBase {
".rio-color-picker-selected-color-label"
)!;
// Subscribe to mouse down events. The other events will be subscribed
// to only once needed.
this.colorSquare.addEventListener(
"mousedown",
this.onSquareMouseDown.bind(this)
);
this.hueBarOuter.addEventListener(
"mousedown",
this.onHueBarMouseDown.bind(this)
);
this.opacityBarOuter.addEventListener(
"mousedown",
this.onOpacityBarMouseDown.bind(this)
);
// Subscribe to pointer down events
this.addDragHandler({
element: this.colorSquare,
onStart: this.onSquarePointerDown.bind(this),
onMove: this.onSquarePointerMove.bind(this),
onEnd: this.onSelectionFinished.bind(this),
});
this.addDragHandler({
element: this.hueBarOuter,
onStart: this.onHueBarPointerDown.bind(this),
onMove: this.onHueBarPointerMove.bind(this),
onEnd: this.onSelectionFinished.bind(this),
});
this.addDragHandler({
element: this.opacityBarOuter,
onStart: this.onOpacityBarPointerDown.bind(this),
onMove: this.onOpacityBarPointerMove.bind(this),
onEnd: this.onSelectionFinished.bind(this),
});
this.selectedColorLabel.addEventListener(
"change",
this.setFromUserHex.bind(this)
@@ -244,63 +250,36 @@ export class ColorPickerComponent extends ComponentBase {
this.matchComponentToSelectedHsv();
}
bindHandler(eventName: string, handler: any) {
let boundHandler = handler.bind(this);
document.addEventListener(eventName, boundHandler);
this.latentEventHandlers.push([eventName, boundHandler]);
}
onSquareMouseDown(event) {
onSquarePointerDown(event): boolean {
this.updateSaturationBrightness(event.clientX, event.clientY);
// Subscribe to other events and keep track of them
this.bindHandler("mousemove", this.onSquareMouseMove);
this.bindHandler("click", this.onSelectionFinished);
// Eat the event
markEventAsHandled(event);
return true;
}
onSquareMouseMove(event) {
onSquarePointerMove(event) {
this.updateSaturationBrightness(event.clientX, event.clientY);
// Eat the event
markEventAsHandled(event);
}
onHueBarMouseDown(event) {
onHueBarPointerDown(event): boolean {
this.updateHue(event.clientX);
// Subscribe to other events and keep track of them
this.bindHandler("mousemove", this.onHueBarMouseMove);
this.bindHandler("click", this.onSelectionFinished);
// Eat the event
markEventAsHandled(event);
return true;
}
onHueBarMouseMove(event) {
onHueBarPointerMove(event) {
this.updateHue(event.clientX);
// Eat the event
markEventAsHandled(event);
}
onOpacityBarMouseDown(event) {
onOpacityBarPointerDown(event): boolean {
this.updateOpacity(event.clientX);
// Subscribe to other events and keep track of them
this.bindHandler("mousemove", this.onOpacityBarMouseMove);
this.bindHandler("click", this.onSelectionFinished);
// Eat the event
markEventAsHandled(event);
return true;
}
onOpacityBarMouseMove(event) {
onOpacityBarPointerMove(event) {
this.updateOpacity(event.clientX);
// Eat the event
markEventAsHandled(event);
}
@@ -310,14 +289,6 @@ export class ColorPickerComponent extends ComponentBase {
color: this.state.color,
});
// Unsubscribe from all events
for (let handler of this.latentEventHandlers) {
let [eventName, boundHandler] = handler;
document.removeEventListener(eventName, boundHandler);
}
this.latentEventHandlers = [];
// Eat the event
markEventAsHandled(event);
}

View File

@@ -10,7 +10,7 @@ import {
import { ComponentId } from "../dataModels";
import { insertWrapperElement, replaceElement } from "../utils";
import { devToolsConnector } from "../app";
import { DialogContainerComponent } from "./dialog_container";
import { DialogContainerComponent } from "./dialogContainer";
/// Base for all component states. Updates received from the backend are
/// partial, hence most properties may be undefined.

View File

@@ -16,7 +16,7 @@ export type ComponentTreeState = ComponentState & {
};
export class ComponentTreeComponent extends ComponentBase {
state: Required<ComponentTreeState>;
declare state: Required<ComponentTreeState>;
private highlighter = new Highlighter();
@@ -467,19 +467,19 @@ export class ComponentTreeComponent extends ComponentBase {
this.highlighter.moveTo(null);
document.documentElement.classList.remove("picking-component");
window.removeEventListener("mousemove", onMouseMove, true);
window.removeEventListener("pointermove", onMouseMove, true);
window.removeEventListener("click", onClick, true);
window.removeEventListener("mousedown", markEventAsHandled, true);
window.removeEventListener("mouseup", markEventAsHandled, true);
window.removeEventListener("pointerdown", markEventAsHandled, true);
window.removeEventListener("pointerup", markEventAsHandled, true);
resolvePromise(undefined);
};
document.documentElement.classList.add("picking-component");
window.addEventListener("mousemove", onMouseMove, true);
window.addEventListener("pointermove", onMouseMove, true);
window.addEventListener("click", onClick, true);
window.addEventListener("mousedown", markEventAsHandled, true);
window.addEventListener("mouseup", markEventAsHandled, true);
window.addEventListener("pointerdown", markEventAsHandled, true);
window.addEventListener("pointerup", markEventAsHandled, true);
await donePicking;
}

View File

@@ -9,7 +9,7 @@ export type CustomListItemState = ComponentState & {
};
export class CustomListItemComponent extends ComponentBase {
state: Required<CustomListItemState>;
declare state: Required<CustomListItemState>;
// If this item has a ripple effect, this is the ripple instance. `null`
// otherwise.

View File

@@ -7,7 +7,7 @@ export type DevToolsConnectorState = ComponentState & {
};
export class DevToolsConnectorComponent extends ComponentBase {
state: Required<DevToolsConnectorState>;
declare state: Required<DevToolsConnectorState>;
// If component tree components exists, they register here
public componentTreeComponent: ComponentTreeComponent | null = null;

View File

@@ -14,7 +14,7 @@ export type DialogContainerState = ComponentState & {
};
export class DialogContainerComponent extends ComponentBase {
state: Required<DialogContainerState>;
declare state: Required<DialogContainerState>;
createElement(): HTMLElement {
// Create the element
@@ -35,6 +35,23 @@ export class DialogContainerComponent extends ComponentBase {
element.addEventListener("click", (event) => {
markEventAsHandled(event);
// Don't close the dialog if the click was inside the dialog. This
// is a bit tricky, because of various cases:
//
// - The click was handled by a component inside of the dialog (e.g.
// a Button). This is simple, since the event will never reach the
// dialog container.
// - The click was onto a component in the dialog, but not handled.
// (Think a `rio.Card`). This must be detected and the dialog NOT
// closed.
// - The click was technically into a component, but that component
// doesn't accept clicks. (Think the spacing of a `rio.Row`.)
// Since no component was technically clicked, the dialog should
// close.
if (event.target !== element) {
return;
}
// Is the dialog user-closable?
if (!this.state.is_user_closable) {
return;

View File

@@ -17,7 +17,7 @@ export type DrawerState = ComponentState & {
};
export class DrawerComponent extends ComponentBase {
state: Required<DrawerState>;
declare state: Required<DrawerState>;
private anchorContainer: HTMLElement;
private contentOuterContainer: HTMLElement;
@@ -66,7 +66,7 @@ export class DrawerComponent extends ComponentBase {
onStart: this.beginDrag.bind(this),
onMove: this.dragMove.bind(this),
onEnd: this.endDrag.bind(this),
// Let things like Buttons and TextInputs consume mouse events
// Let things like Buttons and TextInputs consume pointer events
capturing: false,
});
@@ -212,7 +212,7 @@ export class DrawerComponent extends ComponentBase {
}
}
beginDrag(event: MouseEvent): boolean {
beginDrag(event: PointerEvent): boolean {
// If the drawer isn't user-openable, ignore the click
if (!this.state.is_user_openable) {
return false;
@@ -282,7 +282,7 @@ export class DrawerComponent extends ComponentBase {
return false;
}
dragMove(event: MouseEvent) {
dragMove(event: PointerEvent) {
markEventAsHandled(event);
// Account for the side of the drawer
@@ -313,7 +313,7 @@ export class DrawerComponent extends ComponentBase {
this._updateCss();
}
endDrag(event: MouseEvent): void {
endDrag(event: PointerEvent): void {
markEventAsHandled(event);
// Snap to fully open or fully closed

View File

@@ -1,7 +1,7 @@
import { ComponentBase, ComponentState } from "./componentBase";
import { applyIcon } from "../designApplication";
import { pixelsPerRem } from "../app";
import { InputBox } from "../inputBox";
import { InputBox, InputBoxStyle } from "../inputBox";
import { markEventAsHandled } from "../eventHandling";
import { PopupManager } from "../popupManager";
@@ -10,13 +10,14 @@ export type DropdownState = ComponentState & {
optionNames?: string[];
label?: string;
accessibility_label?: string;
style?: InputBoxStyle;
selectedName?: string;
is_sensitive?: boolean;
is_valid?: boolean;
};
export class DropdownComponent extends ComponentBase {
state: Required<DropdownState>;
declare state: Required<DropdownState>;
private inputBox: InputBox;
private hiddenOptionsElement: HTMLElement;
@@ -70,8 +71,8 @@ export class DropdownComponent extends ComponentBase {
// Connect events
element.addEventListener(
"mousedown",
this._onMouseDown.bind(this),
"pointerdown",
this._onPointerDown.bind(this),
true
);
@@ -230,9 +231,8 @@ export class DropdownComponent extends ComponentBase {
// entering input. Depending on whether they have put in a valid option,
// either save it or reset.
//
// Careful: Clicking on a dropdown option with the mouse also causes us
// to lose focus. If we close the popup too early, the click won't hit
// anything.
// Careful: Clicking on a dropdown option also causes us to lose focus.
// If we close the popup too early, the click won't hit anything.
//
// In Firefox the click is triggered before the focusout, so
// that's no problem. But in Chrome, we have to check whether the focus
@@ -281,7 +281,7 @@ export class DropdownComponent extends ComponentBase {
}
/// Open the dropdown and show all options
private _onMouseDown(event: MouseEvent): void {
private _onPointerDown(event: PointerEvent): void {
// Do we care?
if (!this.state.is_sensitive || event.button !== 0) {
return;
@@ -312,13 +312,13 @@ export class DropdownComponent extends ComponentBase {
// Enter -> select the highlighted option
else if (event.key === "Enter") {
if (this.highlightedOptionElement !== null) {
let mouseDownEvent = new MouseEvent("mousedown", {
let pointerDownEvent = new PointerEvent("pointerdown", {
bubbles: true,
cancelable: true,
view: window,
});
this.highlightedOptionElement.dispatchEvent(mouseDownEvent);
this.highlightedOptionElement.dispatchEvent(pointerDownEvent);
}
}
@@ -485,15 +485,15 @@ export class DropdownComponent extends ComponentBase {
match.classList.add("rio-dropdown-option");
element.appendChild(match);
match.addEventListener("mouseenter", () => {
match.addEventListener("pointerenter", () => {
this._highlightOption(match);
});
// With a `click` handler, the <input> element loses focus for a
// little while, which is noticeable because the floating label will
// quickly move down and then back up. To avoid this, we use
// `mousedown` instead.
match.addEventListener("mousedown", (event) => {
// `pointerdown` instead.
match.addEventListener("pointerdown", (event) => {
this.submitInput(optionName);
markEventAsHandled(event);
});
@@ -554,6 +554,10 @@ export class DropdownComponent extends ComponentBase {
this.inputBox.accessibilityLabel = deltaState.accessibility_label;
}
if (deltaState.style !== undefined) {
this.inputBox.style = deltaState.style;
}
if (deltaState.selectedName !== undefined) {
this.inputBox.value = deltaState.selectedName;
}

View File

@@ -8,7 +8,7 @@ export type BuildFailedState = ComponentState & {
};
export class ErrorPlaceholderComponent extends ComponentBase {
state: Required<BuildFailedState>;
declare state: Required<BuildFailedState>;
private iconElement: HTMLElement;
private summaryElement: HTMLElement;

View File

@@ -86,7 +86,7 @@ type FilePickerAreaState = ComponentState & {
};
export class FilePickerAreaComponent extends ComponentBase {
state: Required<FilePickerAreaState>;
declare state: Required<FilePickerAreaState>;
private fileInput: HTMLInputElement;
private iconElement: HTMLElement;

View File

@@ -11,7 +11,7 @@ export type FlowState = ComponentState & {
};
export class FlowComponent extends ComponentBase {
state: Required<FlowState>;
declare state: Required<FlowState>;
private innerElement: HTMLElement;

View File

@@ -26,7 +26,7 @@ export type FundamentalRootComponentState = ComponentState & {
};
export class FundamentalRootComponent extends ComponentBase {
state: Required<FundamentalRootComponentState>;
declare state: Required<FundamentalRootComponentState>;
public overlaysContainer: HTMLElement;

View File

@@ -19,7 +19,7 @@ export type GridState = ComponentState & {
};
export class GridComponent extends ComponentBase {
state: Required<GridState>;
declare state: Required<GridState>;
createElement(): HTMLElement {
let element = document.createElement("div");

View File

@@ -7,7 +7,7 @@ export type HeadingListItemState = ComponentState & {
};
export class HeadingListItemComponent extends ComponentBase {
state: Required<HeadingListItemState>;
declare state: Required<HeadingListItemState>;
createElement(): HTMLElement {
// Create the element

View File

@@ -7,7 +7,7 @@ export type HighLevelComponentState = ComponentState & {
};
export class HighLevelComponent extends ComponentBase {
state: Required<HighLevelComponentState>;
declare state: Required<HighLevelComponentState>;
createElement(): HTMLElement {
let element = document.createElement("div");

View File

@@ -6,7 +6,7 @@ export type HtmlState = ComponentState & {
};
export class HtmlComponent extends ComponentBase {
state: Required<HtmlState>;
declare state: Required<HtmlState>;
private isInitialized = false;

View File

@@ -15,7 +15,7 @@ export type IconState = ComponentState & {
};
export class IconComponent extends ComponentBase {
state: Required<IconState>;
declare state: Required<IconState>;
private svgElement: SVGSVGElement;

View File

@@ -17,7 +17,7 @@ export type ImageState = ComponentState & {
};
export class ImageComponent extends ComponentBase {
state: Required<ImageState>;
declare state: Required<ImageState>;
private imageElement: HTMLImageElement;
private resizeObserver: ResizeObserver;
@@ -28,7 +28,7 @@ export class ImageComponent extends ComponentBase {
this.imageElement = document.createElement("img");
this.imageElement.role = "img";
// Dragging prevents mouseups and is annoying in general, so we'll
// Dragging prevents pointerups and is annoying in general, so we'll
// disable it
this.imageElement.draggable = false;
element.appendChild(this.imageElement);

View File

@@ -692,7 +692,7 @@ export type KeyEventListenerState = ComponentState & {
};
export class KeyEventListenerComponent extends ComponentBase {
state: Required<KeyEventListenerState>;
declare state: Required<KeyEventListenerState>;
createElement(): HTMLElement {
let element = document.createElement("div");

View File

@@ -13,7 +13,7 @@ export type LayoutDisplayState = ComponentState & {
};
export class LayoutDisplayComponent extends ComponentBase {
state: Required<LayoutDisplayState>;
declare state: Required<LayoutDisplayState>;
// Represents the target component's parent. It matches the aspect ratio of
// the parent and is centered within this component.
@@ -45,13 +45,13 @@ export class LayoutDisplayComponent extends ComponentBase {
// Create the highlighter
this.highlighter = new Highlighter();
// Listen to mouse events
this.parentElement.onmouseenter = () => {
// Listen to pointer events
this.parentElement.onpointerenter = () => {
this.parentIsHovered = true;
this.updateHighlighter();
};
this.parentElement.onmouseleave = () => {
this.parentElement.onpointerleave = () => {
this.parentIsHovered = false;
this.updateHighlighter();
};
@@ -314,12 +314,12 @@ export class LayoutDisplayComponent extends ComponentBase {
};
// Hovering highlights it
childElement.onmouseenter = () => {
childElement.onpointerenter = () => {
this.hoveredChild = childComponent.element;
this.updateHighlighter();
};
childElement.onmouseleave = () => {
childElement.onpointerleave = () => {
if (this.hoveredChild !== childComponent.element) {
return;
}

View File

@@ -14,7 +14,7 @@ export type LinearContainerState = ComponentState & {
const PROPORTIONS_SPACER_SIZE = 30;
export abstract class LinearContainer extends ComponentBase {
state: Required<LinearContainerState>;
declare state: Required<LinearContainerState>;
index = -1; // 0 for Rows, 1 for Columns
sizeAttribute = ""; // 'width' for Rows, 'height' for Columns

View File

@@ -13,7 +13,7 @@ export type LinkState = ComponentState & {
};
export class LinkComponent extends ComponentBase {
state: Required<LinkState>;
declare state: Required<LinkState>;
createElement(): HTMLElement {
let element = document.createElement("a");
@@ -66,7 +66,6 @@ export class LinkComponent extends ComponentBase {
this.removeHtmlChild(latentComponents);
// Add the icon, if any
console.debug(deltaState.icon, this.state.icon);
let icon = deltaState.icon ?? this.state.icon;
if (icon !== null) {

View File

@@ -11,7 +11,7 @@ export type ListViewState = ComponentState & {
};
export class ListViewComponent extends ComponentBase {
state: Required<ListViewState>;
declare state: Required<ListViewState>;
createElement(): HTMLElement {
let element = document.createElement("div");

View File

@@ -113,7 +113,7 @@ function hijackLocalLinks(div: HTMLElement): void {
}
export class MarkdownComponent extends ComponentBase {
state: Required<MarkdownState>;
declare state: Required<MarkdownState>;
createElement(): HTMLElement {
const element = document.createElement("div");

View File

@@ -59,7 +59,7 @@ async function hasAudio(element: HTMLMediaElement): Promise<boolean> {
}
export class MediaPlayerComponent extends ComponentBase {
state: Required<MediaPlayerState>;
declare state: Required<MediaPlayerState>;
private mediaPlayer: HTMLVideoElement;
private altDisplay: HTMLElement;
@@ -383,7 +383,7 @@ export class MediaPlayerComponent extends ComponentBase {
this._updateProgress.bind(this)
);
element.addEventListener("mousemove", this.interact.bind(this), true);
element.addEventListener("pointermove", this.interact.bind(this), true);
element.addEventListener("click", (event: Event) => {
markEventAsHandled(event);
@@ -435,8 +435,8 @@ export class MediaPlayerComponent extends ComponentBase {
});
this.timelineOuter.addEventListener(
"mousemove",
(event: MouseEvent) => {
"pointermove",
(event: PointerEvent) => {
let rect = this.timelineOuter.getBoundingClientRect();
let progress = (event.clientX - rect.left) / rect.width;
this.timelineHover.style.width = `${progress * 100}%`;
@@ -444,7 +444,7 @@ export class MediaPlayerComponent extends ComponentBase {
}
);
this.timelineOuter.addEventListener("mouseleave", () => {
this.timelineOuter.addEventListener("pointerleave", () => {
this.timelineHover.style.opacity = "0";
});
@@ -465,7 +465,7 @@ export class MediaPlayerComponent extends ComponentBase {
this.interact();
this._setVolumeFromMousePosition(event);
this._setVolumeFromPointerPosition(event);
});
this.addDragHandler({
@@ -705,19 +705,19 @@ export class MediaPlayerComponent extends ComponentBase {
this.setVolume(Math.max(humanVolume - 0.1, 0));
}
private _onVolumeDrag(event: MouseEvent): boolean {
private _onVolumeDrag(event: PointerEvent): boolean {
// While dragging, change the volume but don't send it to the backend
// yet
this._notifyBackend = false;
this._setVolumeFromMousePosition(event);
this._setVolumeFromPointerPosition(event);
this.interact();
return true;
}
private _onVolumeDragEnd(event: MouseEvent): void {
private _onVolumeDragEnd(event: PointerEvent): void {
// Now that the user has stopped dragging, send the final volume to the
// backend. We don't need to do anything else, since releasing the mouse
// doesn't change the volume. (Only moving the mouse does.)
// backend. We don't need to do anything else, since releasing the
// pointer doesn't change the volume. (Only moving the pointer does.)
this._notifyBackend = true;
this.setStateAndNotifyBackend({
@@ -725,7 +725,7 @@ export class MediaPlayerComponent extends ComponentBase {
});
}
private _setVolumeFromMousePosition(event: MouseEvent): void {
private _setVolumeFromPointerPosition(event: MouseEvent): void {
let rect = this.volumeOuter.getBoundingClientRect();
let volume = (event.clientX - rect.left) / rect.width;
volume = Math.min(1, Math.max(0, volume));
@@ -737,7 +737,7 @@ export class MediaPlayerComponent extends ComponentBase {
return (event.clientX - rect.left) / rect.width;
}
private _onTimelineDragStart(event: MouseEvent): boolean {
private _onTimelineDragStart(event: PointerEvent): boolean {
this.mediaPlayer.pause(); // Pause the playback while dragging
this._onTimelineDrag(event);
return true;
@@ -751,7 +751,7 @@ export class MediaPlayerComponent extends ComponentBase {
this.interact();
}
private _onTimelineDragEnd(event: MouseEvent): void {
private _onTimelineDragEnd(event: PointerEvent): void {
this.mediaPlayer.play();
}

View File

@@ -33,13 +33,13 @@ export type MouseEventListenerState = ComponentState & {
};
export class MouseEventListenerComponent extends ComponentBase {
state: Required<MouseEventListenerState>;
declare state: Required<MouseEventListenerState>;
private _dragHandler: DragHandler | null = null;
createElement(): HTMLElement {
let element = document.createElement("div");
element.classList.add("rio-mouse-event-listener");
element.classList.add("rio-pointer-event-listener");
return element;
}

View File

@@ -1,5 +1,5 @@
import { markEventAsHandled } from "../eventHandling";
import { InputBox } from "../inputBox";
import { InputBox, InputBoxStyle } from "../inputBox";
import { ComponentBase, ComponentState } from "./componentBase";
export type MultiLineTextInputState = ComponentState & {
@@ -7,12 +7,13 @@ export type MultiLineTextInputState = ComponentState & {
text?: string;
label?: string;
accessibility_label?: string;
style?: InputBoxStyle;
is_sensitive?: boolean;
is_valid?: boolean;
};
export class MultiLineTextInputComponent extends ComponentBase {
state: Required<MultiLineTextInputState>;
declare state: Required<MultiLineTextInputState>;
private inputBox: InputBox;
@@ -29,7 +30,7 @@ export class MultiLineTextInputComponent extends ComponentBase {
});
});
// Detect shift+enter key and send it to the backend
// Detect `shift+enter` and send it to the backend
//
// In addition to notifying the backend, also include the input's
// current value. This ensures any event handlers actually use the up-to
@@ -45,6 +46,22 @@ export class MultiLineTextInputComponent extends ComponentBase {
}
});
// Eat click events so the element can't be clicked-through
element.addEventListener("click", (event) => {
event.stopPropagation();
event.stopImmediatePropagation();
});
element.addEventListener("pointerdown", (event) => {
event.stopPropagation();
event.stopImmediatePropagation();
});
element.addEventListener("pointerup", (event) => {
event.stopPropagation();
event.stopImmediatePropagation();
});
return element;
}
@@ -66,6 +83,10 @@ export class MultiLineTextInputComponent extends ComponentBase {
this.inputBox.accessibilityLabel = deltaState.accessibility_label;
}
if (deltaState.style !== undefined) {
this.inputBox.style = deltaState.style;
}
if (deltaState.is_sensitive !== undefined) {
this.inputBox.isSensitive = deltaState.is_sensitive;
}

View File

@@ -10,7 +10,7 @@ export type NodeInputState = ComponentState & {
};
export class NodeInputComponent extends ComponentBase {
state: Required<NodeInputState>;
declare state: Required<NodeInputState>;
textElement: HTMLElement;
circleElement: HTMLElement;

View File

@@ -10,7 +10,7 @@ export type NodeOutputState = ComponentState & {
};
export class NodeOutputComponent extends ComponentBase {
state: Required<NodeOutputState>;
declare state: Required<NodeOutputState>;
textElement: HTMLElement;
circleElement: HTMLElement;

View File

@@ -8,7 +8,7 @@ export type OverlayState = ComponentState & {
};
export class OverlayComponent extends ComponentBase {
state: Required<OverlayState>;
declare state: Required<OverlayState>;
private overlayElement: HTMLElement;

View File

@@ -22,7 +22,7 @@ type PlotState = ComponentState & {
};
export class PlotComponent extends ComponentBase {
state: Required<PlotState>;
declare state: Required<PlotState>;
// I know this abstraction looks like overkill, but plotly does so much
// stuff with a time delay (loading plotly, setTimeout, resizeObserver, ...)

View File

@@ -0,0 +1,215 @@
import { pixelsPerRem } from "../app";
import { ComponentBase, ComponentState } from "./componentBase";
import { DragHandler } from "../eventHandling";
import { tryGetComponentByElement } from "../componentManagement";
import { ComponentId } from "../dataModels";
import { findComponentUnderMouse } from "../utils";
export type PointerEventListenerState = ComponentState & {
_type_: "PointerEventListener-builtin";
content?: ComponentId;
reportPress: boolean;
reportPointerDown: boolean;
reportPointerUp: boolean;
reportPointerMove: boolean;
reportPointerEnter: boolean;
reportPointerLeave: boolean;
reportDragStart: boolean;
reportDragMove: boolean;
reportDragEnd: boolean;
};
export class PointerEventListenerComponent extends ComponentBase {
declare state: Required<PointerEventListenerState>;
private _dragHandler: DragHandler | null = null;
createElement(): HTMLElement {
let element = document.createElement("div");
element.classList.add("rio-pointer-event-listener");
return element;
}
updateElement(
deltaState: PointerEventListenerState,
latentComponents: Set<ComponentBase>
): void {
super.updateElement(deltaState, latentComponents);
this.replaceOnlyChild(latentComponents, deltaState.content);
if (deltaState.reportPress) {
this.element.onclick = (e) => {
this._sendEventToBackend("press", e as PointerEvent, false);
};
} else {
this.element.onclick = null;
}
if (deltaState.reportPointerDown) {
this.element.onpointerdown = (e) => {
this._sendEventToBackend("pointerDown", e, false);
};
} else {
this.element.onpointerdown = null;
}
if (deltaState.reportPointerUp) {
this.element.onpointerup = (e) => {
this._sendEventToBackend("pointerUp", e, false);
};
} else {
this.element.onpointerup = null;
}
if (deltaState.reportPointerMove) {
this.element.onpointermove = (e) => {
this._sendEventToBackend("pointerMove", e, true);
};
} else {
this.element.onpointermove = null;
}
if (deltaState.reportPointerEnter) {
this.element.onpointerenter = (e) => {
this._sendEventToBackend("pointerEnter", e, false);
};
} else {
this.element.onpointerenter = null;
}
if (deltaState.reportPointerLeave) {
this.element.onpointerleave = (e) => {
this._sendEventToBackend("pointerLeave", e, false);
};
} else {
this.element.onpointerleave = null;
}
if (
deltaState.reportDragStart ||
deltaState.reportDragMove ||
deltaState.reportDragEnd
) {
if (this._dragHandler === null) {
this._dragHandler = this.addDragHandler({
element: this.element,
onStart: this._onDragStart.bind(this),
onMove: this._onDragMove.bind(this),
onEnd: this._onDragEnd.bind(this),
});
}
} else {
if (this._dragHandler !== null) {
this._dragHandler.disconnect();
this._dragHandler = null;
}
}
}
private _onDragStart(event: PointerEvent): boolean {
if (this.state.reportDragStart) {
this._sendEventToBackend("dragStart", event, false);
}
return true;
}
private _onDragMove(event: PointerEvent): void {
if (this.state.reportDragMove) {
this._sendEventToBackend("dragMove", event, true);
}
}
private _onDragEnd(event: PointerEvent): void {
if (this.state.reportDragEnd) {
this._sendEventToBackend("dragEnd", event, false);
}
}
/// Serializes a pointer event to the format expected by Python.
///
/// Not all types of events are supported on the Python side. For example, pen
/// input isn't currently handled. If this particular event isn't supported,
/// returns `null`.
serializePointerEvent(event: PointerEvent): object | null {
// Convert the pointer type
if (event.pointerType !== "mouse" && event.pointerType !== "touch") {
return null;
}
let pointerType = event.pointerType;
// Convert the button
if (event.button < 0 || event.button > 2) {
return null;
}
let button = ["left", "middle", "right"][event.button];
// Get the event positions
let elementRect = this.element.getBoundingClientRect();
let windowX = event.clientX / pixelsPerRem;
let windowY = event.clientY / pixelsPerRem;
let componentX = windowX - elementRect.left / pixelsPerRem;
let componentY = windowY - elementRect.top / pixelsPerRem;
// Build the result
return {
pointerType: pointerType,
button: button,
windowX: windowX,
windowY: windowY,
componentX: componentX,
componentY: componentY,
};
}
/// Serializes a pointer event to the format expected by Python. Follows the
/// same semantics as `serializePointerEvent`.
serializePointerMoveEvent(event: PointerEvent): object | null {
// Serialize this as a pointer event
let result = this.serializePointerEvent(event);
// Did the serialization succeed?
if (result === null) {
return null;
}
// Add the relative position
result["relativeX"] = event.movementX / pixelsPerRem;
result["relativeY"] = event.movementY / pixelsPerRem;
// Done
return result;
}
/// Serializes the given event and sends it to the backend. If this type of
/// event isn't supported by the backend (e.g. pen inputs), does nothing.
private _sendEventToBackend(
eventType: string,
event: PointerEvent,
asMoveEvent: boolean
): void {
// Serialize the event
let serialized: object | null;
if (asMoveEvent) {
serialized = this.serializePointerMoveEvent(event);
} else {
serialized = this.serializePointerEvent(event);
}
// Did the serialization succeed?
if (serialized === null) {
return;
}
// Send the event
if (serialized !== null) {
this.sendMessageToBackend({
type: eventType,
...serialized,
});
}
}
}

View File

@@ -23,7 +23,7 @@ export type PopupState = ComponentState & {
};
export class PopupComponent extends ComponentBase {
state: Required<PopupState>;
declare state: Required<PopupState>;
private anchorContainer: HTMLElement;
private contentContainer: HTMLElement;

View File

@@ -10,7 +10,7 @@ export type ProgressBarState = ComponentState & {
};
export class ProgressBarComponent extends ComponentBase {
state: Required<ProgressBarState>;
declare state: Required<ProgressBarState>;
fillElement: HTMLElement;

View File

@@ -9,7 +9,7 @@ export type ProgressCircleState = ComponentState & {
};
export class ProgressCircleComponent extends ComponentBase {
state: Required<ProgressCircleState>;
declare state: Required<ProgressCircleState>;
createElement(): HTMLElement {
let element = document.createElement("div");

View File

@@ -72,7 +72,7 @@ function cursorToCSS(cursor: string): string {
}
export class RectangleComponent extends ComponentBase {
state: Required<RectangleState>;
declare state: Required<RectangleState>;
// If this rectangle has a ripple effect, this is the ripple instance.
// `null` otherwise.

View File

@@ -16,7 +16,7 @@ export type RevealerState = ComponentState & {
};
export class RevealerComponent extends ComponentBase {
state: Required<RevealerState>;
declare state: Required<RevealerState>;
private headerElement: HTMLElement;
private labelElement: HTMLElement;
@@ -81,11 +81,11 @@ export class RevealerComponent extends ComponentBase {
};
// Color change on hover/leave
this.headerElement.onmouseenter = () => {
this.headerElement.onpointerenter = () => {
this.element.style.background = "var(--rio-local-bg-variant)";
};
this.headerElement.onmouseleave = () => {
this.headerElement.onpointerleave = () => {
this.element.style.removeProperty("background");
};

View File

@@ -12,7 +12,7 @@ export type ScrollContainerState = ComponentState & {
};
export class ScrollContainerComponent extends ComponentBase {
state: Required<ScrollContainerState>;
declare state: Required<ScrollContainerState>;
private scrollerElement: HTMLElement;
private childContainer: HTMLElement;

View File

@@ -13,7 +13,7 @@ export type ScrollTargetState = ComponentState & {
};
export class ScrollTargetComponent extends ComponentBase {
state: Required<ScrollTargetState>;
declare state: Required<ScrollTargetState>;
childContainerElement: HTMLElement;
buttonContainerElement: HTMLElement;

View File

@@ -9,7 +9,7 @@ export type SeparatorState = ComponentState & {
};
export class SeparatorComponent extends ComponentBase {
state: Required<SeparatorState>;
declare state: Required<SeparatorState>;
createElement(): HTMLElement {
let element = document.createElement("div");

View File

@@ -5,7 +5,7 @@ export type SeparatorListItemState = ComponentState & {
};
export class SeparatorListItemComponent extends ComponentBase {
state: Required<SeparatorListItemState>;
declare state: Required<SeparatorListItemState>;
createElement(): HTMLElement {
let element = document.createElement("div");

View File

@@ -14,7 +14,7 @@ export type SliderState = ComponentState & {
};
export class SliderComponent extends ComponentBase {
state: Required<SliderState>;
declare state: Required<SliderState>;
private innerElement: HTMLElement;
private minValueElement: HTMLElement;
@@ -63,7 +63,7 @@ export class SliderComponent extends ComponentBase {
return element;
}
private setValueFromMouseEvent(event: MouseEvent): [number, number] {
private setValueFromPointerEvent(event: PointerEvent): [number, number] {
// If the slider is disabled, do nothing
if (!this.state.is_sensitive) {
return [this.state.value, this.state.value];
@@ -101,14 +101,14 @@ export class SliderComponent extends ComponentBase {
return [fraction, newValue];
}
private onDragStart(event: MouseEvent): boolean {
private onDragStart(event: PointerEvent): boolean {
markEventAsHandled(event);
this.setValueFromMouseEvent(event);
this.setValueFromPointerEvent(event);
return true;
}
private onDragMove(event: MouseEvent): void {
private onDragMove(event: PointerEvent): void {
markEventAsHandled(event);
// Make future transitions instant to avoid lag
@@ -117,10 +117,10 @@ export class SliderComponent extends ComponentBase {
"0s"
);
this.setValueFromMouseEvent(event);
this.setValueFromPointerEvent(event);
}
private onDragEnd(event: MouseEvent): void {
private onDragEnd(event: PointerEvent): void {
markEventAsHandled(event);
// Revert to the default transition time
@@ -129,7 +129,7 @@ export class SliderComponent extends ComponentBase {
);
// Get the new value
let [fraction, value] = this.setValueFromMouseEvent(event);
let [fraction, value] = this.setValueFromPointerEvent(event);
// Update state and notify the backend of the new value
this.setStateAndNotifyBackend({

View File

@@ -13,7 +13,7 @@ export type SlideshowState = ComponentState & {
};
export class SlideshowComponent extends ComponentBase {
state: Required<SlideshowState>;
declare state: Required<SlideshowState>;
private childContainer: HTMLElement;
private progressBar: HTMLElement;
@@ -51,11 +51,11 @@ export class SlideshowComponent extends ComponentBase {
) as HTMLElement;
// Connect to events
element.addEventListener("mouseenter", () => {
element.addEventListener("pointerenter", () => {
this.isPaused = true;
});
element.addEventListener("mouseleave", () => {
element.addEventListener("pointerleave", () => {
this.isPaused = false;
});

View File

@@ -7,7 +7,7 @@ export type StackState = ComponentState & {
};
export class StackComponent extends ComponentBase {
state: Required<StackState>;
declare state: Required<StackState>;
createElement(): HTMLElement {
let element = document.createElement("div");

View File

@@ -8,7 +8,7 @@ export type SwitchState = ComponentState & {
};
export class SwitchComponent extends ComponentBase {
state: Required<SwitchState>;
declare state: Required<SwitchState>;
createElement(): HTMLElement {
let element = document.createElement("div");

View File

@@ -10,7 +10,7 @@ export type SwitcherState = ComponentState & {
};
export class SwitcherComponent extends ComponentBase {
state: Required<SwitcherState>;
declare state: Required<SwitcherState>;
private activeChildContainer: HTMLElement | null = null;
private resizerElement: HTMLElement | null = null;

View File

@@ -19,7 +19,7 @@ export type SwitcherBarState = ComponentState & {
};
export class SwitcherBarComponent extends ComponentBase {
state: Required<SwitcherBarState>;
declare state: Required<SwitcherBarState>;
private innerElement: HTMLElement; // Used for alignment
private markerElement: HTMLElement; // Highlights the selected item
@@ -356,7 +356,6 @@ export class SwitcherBarComponent extends ComponentBase {
if (deltaState.selectedName !== this.state.selectedName) {
this.state.selectedName = deltaState.selectedName;
this.state.names = deltaState.names ?? this.state.names;
this.animateToCurrentTarget();
}
} else if (deltaState.selectedName === null) {

View File

@@ -21,7 +21,7 @@ type TableState = ComponentState & {
};
export class TableComponent extends ComponentBase {
state: Required<TableState>;
declare state: Required<TableState>;
private tableElement: HTMLElement;
@@ -48,8 +48,6 @@ export class TableComponent extends ComponentBase {
// Content
if (deltaState.data !== undefined) {
console.log(`Headers ${deltaState.headers}`);
console.log(`Data ${deltaState.data}`);
this.updateContent();
// Since the content was completely replaced, there is no need to
@@ -192,11 +190,11 @@ export class TableComponent extends ComponentBase {
let yy = Math.floor(ii / htmlWidth);
let cellElement = this.tableElement.children[ii] as HTMLElement;
cellElement.addEventListener("mouseenter", () => {
cellElement.addEventListener("pointerenter", () => {
this.onEnterCell(cellElement, xx, yy);
});
cellElement.addEventListener("mouseleave", () => {
cellElement.addEventListener("pointerleave", () => {
this.onLeaveCell(cellElement, xx, yy);
});
}

View File

@@ -12,7 +12,7 @@ export type TextState = ComponentState & {
};
export class TextComponent extends ComponentBase {
state: Required<TextState>;
declare state: Required<TextState>;
private inner: HTMLElement;

View File

@@ -1,6 +1,6 @@
import { ComponentBase, ComponentState } from "./componentBase";
import { Debouncer } from "../debouncer";
import { InputBox } from "../inputBox";
import { InputBox, InputBoxStyle } from "../inputBox";
import { markEventAsHandled } from "../eventHandling";
export type TextInputState = ComponentState & {
@@ -8,7 +8,7 @@ export type TextInputState = ComponentState & {
text?: string;
label?: string;
accessibility_label?: string;
style?: "rectangular" | "pill";
style?: InputBoxStyle;
prefix_text?: string;
suffix_text?: string;
is_secret?: boolean;
@@ -17,7 +17,7 @@ export type TextInputState = ComponentState & {
};
export class TextInputComponent extends ComponentBase {
state: Required<TextInputState>;
declare state: Required<TextInputState>;
private inputBox: InputBox;
private onChangeLimiter: Debouncer;
@@ -66,7 +66,7 @@ export class TextInputComponent extends ComponentBase {
});
});
// Detect the enter key and send them to the backend
// Detect `enter` and send them to the backend
//
// In addition to notifying the backend, also include the input's
// current value. This ensures any event handlers actually use the up-to
@@ -101,12 +101,12 @@ export class TextInputComponent extends ComponentBase {
event.stopImmediatePropagation();
});
element.addEventListener("mousedown", (event) => {
element.addEventListener("pointerdown", (event) => {
event.stopPropagation();
event.stopImmediatePropagation();
});
element.addEventListener("mouseup", (event) => {
element.addEventListener("pointerup", (event) => {
event.stopPropagation();
event.stopImmediatePropagation();
});
@@ -132,6 +132,10 @@ export class TextInputComponent extends ComponentBase {
this.inputBox.accessibilityLabel = deltaState.accessibility_label;
}
if (deltaState.style !== undefined) {
this.inputBox.style = deltaState.style;
}
if (deltaState.prefix_text !== undefined) {
this.inputBox.prefixText = deltaState.prefix_text;
}
@@ -153,21 +157,6 @@ export class TextInputComponent extends ComponentBase {
if (deltaState.is_valid !== undefined) {
this.inputBox.isValid = deltaState.is_valid;
}
// TODO: This isn't exposed to Python yet, so pretend the attribute
// exists by setting it here.
deltaState.style = "rectangular";
if (deltaState.style !== undefined) {
this.element.classList.remove(
"rio-input-box-style-rectangle",
"rio-input-box-style-pill"
);
this.element.classList.add(
`rio-input-box-style-${this.state.style}`
);
}
}
grabKeyboardFocus(): void {

View File

@@ -9,7 +9,7 @@ export type ThemeContextSwitcherState = ComponentState & {
};
export class ThemeContextSwitcherComponent extends ComponentBase {
state: Required<ThemeContextSwitcherState>;
declare state: Required<ThemeContextSwitcherState>;
createElement(): HTMLElement {
let element = document.createElement("div");

View File

@@ -11,7 +11,7 @@ export type TooltipState = ComponentState & {
};
export class TooltipComponent extends ComponentBase {
state: Required<TooltipState>;
declare state: Required<TooltipState>;
private popupElement: HTMLElement;
private popupManager: PopupManager;
@@ -29,11 +29,11 @@ export class TooltipComponent extends ComponentBase {
);
// Listen for events
element.addEventListener("mouseover", () => {
element.addEventListener("pointerenter", () => {
this.popupManager.isOpen = true;
});
element.addEventListener("mouseout", () => {
element.addEventListener("pointerleave", () => {
this.popupManager.isOpen = false;
});

View File

@@ -6,11 +6,13 @@ export type WebsiteState = ComponentState & {
};
export class WebsiteComponent extends ComponentBase {
state: Required<WebsiteState>;
declare state: Required<WebsiteState>;
element: HTMLIFrameElement;
createElement(): HTMLElement {
return document.createElement("iframe");
let element = document.createElement("iframe");
element.classList.add("rio-website");
return element;
}
updateElement(

View File

@@ -101,7 +101,7 @@ export function textStyleToCss(
let fontSize: string;
let fontWeight: string;
let fontStyle: string;
let textDecoration: string;
let textDecorations: string[] = [];
let textTransform: string;
let color: string;
let background: string;
@@ -136,7 +136,7 @@ export function textStyleToCss(
fontFamily = globalPrefix + "font-name)";
fontSize = globalPrefix + "font-size)";
fontStyle = globalPrefix + "font-italic)";
textDecoration = globalPrefix + "underlined)";
textDecorations.push(globalPrefix + "text-decoration)");
textTransform = globalPrefix + "all-caps)";
}
@@ -145,7 +145,15 @@ export function textStyleToCss(
fontSize = style.fontSize + "em";
fontStyle = style.italic ? "italic" : "normal";
fontWeight = style.fontWeight;
textDecoration = style.underlined ? "underline" : "none";
if (style.underlined) {
textDecorations.push("underline");
}
if (style.strikethrough) {
textDecorations.push("line-through");
}
textTransform = style.allCaps ? "uppercase" : "none";
// If no font family is provided, stick to the theme's.
@@ -197,7 +205,8 @@ export function textStyleToCss(
"font-size": fontSize,
"font-weight": fontWeight,
"font-style": fontStyle,
"text-decoration": textDecoration,
"text-decoration":
textDecorations.length > 0 ? textDecorations.join(" ") : "none",
"text-transform": textTransform,
color: color,
background: background,

View File

@@ -60,6 +60,7 @@ export type TextStyle = {
italic: boolean;
fontWeight: "normal" | "bold";
underlined: boolean;
strikethrough: boolean;
allCaps: boolean;
};

View File

@@ -29,13 +29,13 @@ function _no_op(): boolean {
}
export type ClickHandlerArguments = {
onClick: (event: MouseEvent) => boolean;
onClick: (event: PointerEvent) => boolean;
target?: EventTarget;
capturing?: boolean;
};
export class ClickHandler extends EventHandler {
private onClick: (event: MouseEvent) => void;
private onClick: (event: PointerEvent) => void;
private target: EventTarget;
private capturing: boolean;
@@ -60,22 +60,22 @@ export class ClickHandler extends EventHandler {
export type DragHandlerArguments = {
element: HTMLElement;
onStart?: (event: MouseEvent) => boolean;
onMove?: (event: MouseEvent) => void;
onEnd?: (event: MouseEvent) => void;
onStart?: (event: PointerEvent) => boolean;
onMove?: (event: PointerEvent) => void;
onEnd?: (event: PointerEvent) => void;
capturing?: boolean;
};
export class DragHandler extends EventHandler {
private element: HTMLElement;
private onStart: (event: MouseEvent) => boolean;
private onMove: (event: MouseEvent) => void;
private onEnd: (event: MouseEvent) => void;
private onStart: (event: PointerEvent) => boolean;
private onMove: (event: PointerEvent) => void;
private onEnd: (event: PointerEvent) => void;
private capturing: boolean;
private onMouseDown = this._onMouseDown.bind(this);
private onMouseMove = this._onMouseMove.bind(this);
private onMouseUp = this._onMouseUp.bind(this);
private onPointerDown = this._onPointerDown.bind(this);
private onPointerMove = this._onPointerMove.bind(this);
private onPointerUp = this._onPointerUp.bind(this);
private onClick = this._onClick.bind(this);
private hasDragged = false;
@@ -91,15 +91,15 @@ export class DragHandler extends EventHandler {
this.capturing = args.capturing ?? true;
this.element.addEventListener(
"mousedown",
this.onMouseDown,
"pointerdown",
this.onPointerDown,
this.capturing
);
}
private _onMouseDown(event: MouseEvent): void {
// We only care about the left mouse button
if (event.button !== 0) {
private _onPointerDown(event: PointerEvent): void {
// On mice we only care about the left mouse button
if (event.pointerType === "mouse" && event.button !== 0) {
return;
}
@@ -120,12 +120,12 @@ export class DragHandler extends EventHandler {
markEventAsHandled(event);
window.addEventListener("mousemove", this.onMouseMove, true);
window.addEventListener("mouseup", this.onMouseUp, true);
window.addEventListener("pointermove", this.onPointerMove, true);
window.addEventListener("pointerup", this.onPointerUp, true);
window.addEventListener("click", this.onClick, true);
}
private _onMouseMove(event: MouseEvent): void {
private _onPointerMove(event: PointerEvent): void {
this.hasDragged = true;
markEventAsHandled(event);
@@ -133,7 +133,7 @@ export class DragHandler extends EventHandler {
this.onMove(event);
}
private _onMouseUp(event: MouseEvent): void {
private _onPointerUp(event: PointerEvent): void {
if (this.hasDragged) {
markEventAsHandled(event);
}
@@ -142,7 +142,7 @@ export class DragHandler extends EventHandler {
this.onEnd(event);
}
private _onClick(event: MouseEvent): void {
private _onClick(event: PointerEvent): void {
// This event isn't using by the drag event handler, but it should
// nonetheless be stopped to prevent the click from being handled by
// other handlers.
@@ -153,8 +153,8 @@ export class DragHandler extends EventHandler {
}
private _disconnectDragListeners(): void {
window.removeEventListener("mousemove", this.onMouseMove, true);
window.removeEventListener("mouseup", this.onMouseUp, true);
window.removeEventListener("pointermove", this.onPointerMove, true);
window.removeEventListener("pointerup", this.onPointerUp, true);
window.removeEventListener("click", this.onClick, true);
}
@@ -162,8 +162,8 @@ export class DragHandler extends EventHandler {
super.disconnect();
this.element.removeEventListener(
"mousedown",
this.onMouseDown,
"pointerdown",
this.onPointerDown,
this.capturing
);
this._disconnectDragListeners();

View File

@@ -1,5 +1,6 @@
import { markEventAsHandled, stopPropagation } from "./eventHandling";
import { createUniqueId } from "./utils";
export type InputBoxStyle = "underlined" | "rounded" | "pill";
/// A text input field providing the following features and more:
///
@@ -33,7 +34,10 @@ export class InputBox {
connectClickHandlers?: boolean;
} = {}) {
this.outerElement = document.createElement("div");
this.outerElement.classList.add("rio-input-box");
this.outerElement.classList.add(
"rio-input-box",
"rio-input-box-style-underlined"
);
this.outerElement.innerHTML = `
<div class="rio-input-box-padding"></div>
@@ -106,27 +110,24 @@ export class InputBox {
}
private connectClickHandlers(): void {
// Detect clicks on any part of the component and focus the input
//
// The `mousedown` are needed to prevent any potential drag events from
// starting.
// Consider any clicks on the input box as handled. This prevents e.g.
// drag events when trying to select something.
this.prefixTextElement.addEventListener(
"mousedown",
"pointerdown",
markEventAsHandled
);
this.suffixTextElement.addEventListener(
"mousedown",
this.suffixElementContainer.addEventListener(
"pointerdown",
markEventAsHandled
);
// The `click` events pass focus to the input and move the cursor.
// This has to be done in `mouseup`, rather than `mousedown`, because
// otherwise the browser removes the focus again on mouseup.
// When clicked, focus the text element and move the cursor accordingly.
let selectStart = (event: Event) => {
this._inputElement.focus();
this._inputElement.setSelectionRange(0, 0);
markEventAsHandled(event);
};
this.suffixElementContainer;
this.prefixTextElement.addEventListener("click", selectStart);
let selectEnd = (event: Event) => {
@@ -137,7 +138,6 @@ export class InputBox {
);
markEventAsHandled(event);
};
this.suffixElementContainer.addEventListener("click", selectEnd);
this.suffixTextElement.addEventListener("click", selectEnd);
@@ -147,10 +147,10 @@ export class InputBox {
paddingLeft.addEventListener("click", selectStart);
paddingRight.addEventListener("click", selectEnd);
// Mousedown selects the input element and/or text in it (via dragging),
// so let it do its default behavior but then stop it from propagating
// to other elements
this._inputElement.addEventListener("mousedown", stopPropagation);
// Pointer down events select the input element and/or text in it (via
// dragging), so let them do their default behavior but then stop them
// from propagating to other elements
this._inputElement.addEventListener("pointerdown", stopPropagation);
}
get inputElement(): HTMLInputElement {
@@ -229,6 +229,16 @@ export class InputBox {
}
}
set style(style: InputBoxStyle) {
this.outerElement.classList.remove(
"rio-input-box-style-underlined",
"rio-input-box-style-rounded",
"rio-input-box-style-pill"
);
this.outerElement.classList.add(`rio-input-box-style-${style}`);
}
get isSensitive(): boolean {
return !this._inputElement.disabled;
}

View File

@@ -417,7 +417,7 @@ export async function processMessageReturnResponse(
message.params.themeVariant
);
// Remove the default anti-flashbang gray
// Remove the default anti-flashbang color
document.documentElement.style.background = "";
response = null;

View File

@@ -274,6 +274,8 @@ select {
// User-defined components
.rio-high-level-component {
pointer-events: none;
@include single-container();
}
@@ -492,8 +494,8 @@ select {
@include single-container();
}
// Mouse event listener
.rio-mouse-event-listener {
// Pointer event listener
.rio-pointer-event-listener {
@include single-container();
}
@@ -558,11 +560,26 @@ $rio-input-box-small-label-spacing-top: 0.5rem;
background-color: var(--rio-local-bg-variant);
transition: background-color 0.1s linear;
}
.rio-input-box-style-underlined {
border-radius: var(--rio-global-corner-radius-small)
var(--rio-global-corner-radius-small) 0 0;
}
.rio-input-box-style-rounded {
border-radius: var(--rio-global-corner-radius-small);
}
.rio-input-box-style-pill {
border-radius: $infinite-corner-radius;
}
*:not(.rio-input-box-style-underlined) > .rio-input-box-plain-bar,
*:not(.rio-input-box-style-underlined) > .rio-input-box-color-bar {
display: none;
}
.rio-input-box:hover:not(.rio-insensitive) {
background-color: var(--rio-local-bg-active);
}
@@ -1205,9 +1222,10 @@ $rio-input-box-small-label-spacing-top: 0.5rem;
.rio-button {
pointer-events: auto;
// Preserve the text color outside of the switcheroo application, as some
// styles depend on it.
// Preserve some colors outside of the switcheroo application, as some
// styles depend on them.
--outer-text-color: var(--rio-local-text-color);
--outer-bg-active-color: var(--rio-local-bg-active);
transition:
color 0.1s ease-in-out,
border-color 0.1s ease-in-out;
@@ -1251,6 +1269,11 @@ $rio-input-box-small-label-spacing-top: 0.5rem;
.rio-buttonstyle-minor {
border: 0.1rem solid var(--rio-local-bg);
--rio-local-text-color: var(--rio-local-bg);
// Note the lack of transition here. While a transition would be nice, the
// text & icon wouldn't animate alongside the background, because they're
// independent high-level components. Having just the background transition
// but the foreground switch immediately is jarring.
}
.rio-buttonstyle-minor:hover {
@@ -1266,6 +1289,17 @@ $rio-input-box-small-label-spacing-top: 0.5rem;
--rio-local-text-color: var(--rio-global-disabled-bg) !important;
}
.rio-buttonstyle-colored-text,
.rio-buttonstyle-plain-text {
&:hover {
cursor: pointer;
position: relative;
background-color: var(--outer-bg-active-color);
}
transition: background-color 0.1s ease-in-out;
}
.rio-buttonstyle-colored-text {
--rio-local-text-color: var(--rio-local-bg);
}
@@ -1274,30 +1308,6 @@ $rio-input-box-small-label-spacing-top: 0.5rem;
--rio-local-text-color: var(--outer-text-color);
}
.rio-buttonstyle-colored-text:hover,
.rio-buttonstyle-plain-text:hover {
cursor: pointer;
--rio-local-text-color: var(--rio-local-bg);
position: relative;
&::after {
content: "";
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
z-index: -1;
background-color: var(--outer-text-color);
opacity: 0.1;
border-radius: var(--border-radius);
}
}
.rio-buttonstyle-plain-text.rio-insensitive {
cursor: default;
--rio-local-text-color: var(--rio-global-disabled-bg);
@@ -1992,7 +2002,7 @@ $rio-input-box-small-label-spacing-top: 0.5rem;
code {
font-family: $monospace-fonts;
background: var(--rio-local-bg-variant);
border-radius: var(--rio-global-corner-radius-small);
border-radius: var(--rio-global-corner-radius-medium);
padding: 0.1rem 0.3rem;
}
@@ -2002,7 +2012,7 @@ $rio-input-box-small-label-spacing-top: 0.5rem;
font-size: var(--rio-global-heading1-font-size);
font-style: var(--rio-global-heading1-italic);
font-weight: var(--rio-global-heading1-font-weight);
text-decoration: var(--rio-global-heading1-underlined);
text-decoration: var(--rio-global-heading1-text-decoration);
text-transform: var(--rio-global-heading1-all-caps);
&:not(:first-child) {
@@ -2020,7 +2030,7 @@ $rio-input-box-small-label-spacing-top: 0.5rem;
font-size: var(--rio-global-heading2-font-size);
font-style: var(--rio-global-heading2-italic);
font-weight: var(--rio-global-heading2-font-weight);
text-decoration: var(--rio-global-heading2-underlined);
text-decoration: var(--rio-global-heading2-text-decoration);
text-transform: var(--rio-global-heading2-all-caps);
margin-top: 0;
@@ -2040,7 +2050,7 @@ $rio-input-box-small-label-spacing-top: 0.5rem;
font-size: var(--rio-global-heading3-font-size);
font-style: var(--rio-global-heading3-italic);
font-weight: var(--rio-global-heading3-font-weight);
text-decoration: var(--rio-global-heading3-underlined);
text-decoration: var(--rio-global-heading3-text-decoration);
text-transform: var(--rio-global-heading3-all-caps);
margin-top: 0;
@@ -2061,7 +2071,7 @@ $rio-input-box-small-label-spacing-top: 0.5rem;
line-height: 1.35em; // Purposely uses em
font-style: var(--rio-global-text-italic);
font-weight: var(--rio-global-text-font-weight);
text-decoration: var(--rio-global-text-underlined);
text-decoration: var(--rio-global-text-text-decoration);
text-transform: var(--rio-global-text-all-caps);
}
@@ -2125,10 +2135,10 @@ $rio-input-box-small-label-spacing-top: 0.5rem;
flex-direction: column;
align-items: stretch;
gap: 0.5rem;
padding: 0.5rem;
padding: var(--rio-global-corner-radius-medium);
background: var(--rio-local-bg-variant);
border-radius: var(--rio-global-corner-radius-small);
border-radius: var(--rio-global-corner-radius-medium);
box-sizing: border-box;
}
@@ -2156,7 +2166,6 @@ $rio-input-box-small-label-spacing-top: 0.5rem;
cursor: pointer;
border: none;
background: none;
border-radius: var(--rio-global-corner-radius-small);
margin: 0;
padding: 0;
opacity: 0.4;
@@ -2244,7 +2253,7 @@ $rio-input-box-small-label-spacing-top: 0.5rem;
min-height: 6rem;
cursor: crosshair;
margin-bottom: 0.7rem;
border-radius: var(--rio-global-corner-radius-small);
border-radius: var(--rio-global-corner-radius-medium);
flex-grow: 1;
}
@@ -2451,7 +2460,7 @@ $rio-input-box-small-label-spacing-top: 0.5rem;
// the drawer content
z-index: 2;
box-shadow: 0 0 1rem var(--rio-global-shadow-color);
box-shadow: 0 0 1.3rem var(--rio-global-shadow-color);
transition: transform 0.3s ease-out;
}
@@ -2473,6 +2482,10 @@ $rio-input-box-small-label-spacing-top: 0.5rem;
.rio-drawer-left,
.rio-drawer-right {
.rio-drawer-content-inner {
overflow-y: auto;
}
.rio-drawer-knob {
width: 0.4rem;
height: 4rem;
@@ -2481,6 +2494,10 @@ $rio-input-box-small-label-spacing-top: 0.5rem;
.rio-drawer-top,
.rio-drawer-bottom {
.rio-drawer-content-inner {
overflow-x: auto;
}
.rio-drawer-knob {
width: 4rem;
height: 0.4rem;
@@ -2594,7 +2611,7 @@ $rio-input-box-small-label-spacing-top: 0.5rem;
}
.rio-card-elevate-on-hover:hover {
box-shadow: 0 0.15rem 0.3rem var(--rio-global-shadow-color);
box-shadow: 0 0.15rem 0.4rem var(--rio-global-shadow-color);
}
.rio-card-colorize-on-hover:hover {
@@ -2871,7 +2888,7 @@ $rio-input-box-small-label-spacing-top: 0.5rem;
overflow-wrap: break-word;
padding: 0.5rem 1rem;
background-color: var(--rio-local-bg-variant);
border-radius: var(--rio-global-corner-radius-small);
border-radius: var(--rio-global-corner-radius-medium);
}
.rio-traceback-footer {
@@ -3276,7 +3293,7 @@ html.picking-component * {
align-items: stretch;
background: var(--rio-global-danger-bg);
border-radius: var(--rio-global-corner-radius-small);
border-radius: var(--rio-global-corner-radius-medium);
// `rio-error-placeholder-content` can't have a corner radius set, because that
// would make the barber pole peek through the corners. Instead enforce
@@ -3795,6 +3812,11 @@ html.picking-component * {
}
}
// Website
.rio-website {
pointer-events: auto;
}
// Upload Area
.rio-file-picker-area {
pointer-events: auto;

View File

@@ -1,10 +1,5 @@
<!doctype html>
<!--
Until the CSS is loaded and the server sends us the theme colors, make the
background dark so that dark mode users don't get flashbanged by a completely
white screen
-->
<html data-theme="dark" style="background: #333">
<html>
<head>
<title>{title}</title>
<meta name="{meta}" />
@@ -26,4 +21,28 @@ white screen
</head>
<body class="rio-switcheroo-background"></body>
<script>
/// Apply the theme's background color. This is done immediately in the
/// `index.html` to avoid the users getting flashbanged by a completely
/// white screen while loading.
const LIGHT_THEME_BACKGROUND_COLOR = "{light_theme_background_color}";
const DARK_THEME_BACKGROUND_COLOR = "{dark_theme_background_color}";
let useLightTheme = !window.matchMedia("(prefers-color-scheme: dark)")
.matches;
let themeVariables;
// Apply the background color and also set a data attribute on the HTML.
// This will be used by CSS to switch code highlighting themes.
if (useLightTheme) {
document.documentElement.style.backgroundColor =
LIGHT_THEME_BACKGROUND_COLOR;
document.documentElement.setAttribute("data-theme", "light");
} else {
document.documentElement.style.backgroundColor =
DARK_THEME_BACKGROUND_COLOR;
document.documentElement.setAttribute("data-theme", "dark");
}
</script>
</html>

View File

@@ -31,6 +31,7 @@ dependencies = [
"crawlerdetect~=0.1.7",
"ordered-set>=4.1.0",
"imy[docstrings]>=0.4.0",
"path-imports>=1.1.1",
]
requires-python = ">= 3.10"
readme = "README.md"
@@ -106,6 +107,7 @@ dev-dependencies = [
"ruff>=0.4.7",
"selenium>=4.22",
"hatch>=1.11.1",
"pyfakefs>=5.7.1",
]
managed = true

View File

@@ -1,4 +1,4 @@
__version__ = "0.10.2"
__version__ = "0.10.5rc0"
# There is an issue with `rye test`. rye passes a `--rootdir` argument to

View File

@@ -1,13 +1,13 @@
from __future__ import annotations
import functools
import os
import sys
import threading
import typing as t
import webbrowser
from collections.abc import Callable, Iterable
from datetime import timedelta
from pathlib import Path
from typing import * # type: ignore
import fastapi
import uvicorn
@@ -15,7 +15,7 @@ import uvicorn
import __main__
import rio
from . import assets, maybes, routing, utils
from . import assets, global_state, maybes, routing, utils
from .app_server import fastapi_server
from .utils import ImageLike
@@ -60,7 +60,7 @@ def make_default_connection_lost_component() -> rio.Component:
return DefaultConnectionLostComponent()
@final
@t.final
class App:
"""
Contains all the information needed to run a Rio app.
@@ -130,18 +130,18 @@ class App:
# Type hints so the documentation generator knows which fields exist
name: str
description: str
pages: tuple[rio.ComponentPage | rio.Redirect, ...]
assets_dir: Path
pages: t.Sequence[rio.ComponentPage | rio.Redirect]
meta_tags: dict[str, str]
def __init__(
self,
*,
build: Callable[[], rio.Component] | None = None,
build: t.Callable[[], rio.Component] | None = None,
name: str | None = None,
description: str | None = None,
icon: ImageLike | None = None,
pages: Iterable[rio.ComponentPage | rio.Redirect]
pages: t.Iterable[rio.ComponentPage | rio.Redirect]
| os.PathLike
| str
| None = None,
@@ -149,11 +149,11 @@ class App:
on_app_close: rio.EventHandler[App] = None,
on_session_start: rio.EventHandler[rio.Session] = None,
on_session_close: rio.EventHandler[rio.Session] = None,
default_attachments: Iterable[Any] = (),
default_attachments: t.Iterable[t.Any] = (),
ping_pong_interval: int | float | timedelta = timedelta(seconds=50),
assets_dir: str | os.PathLike = "assets",
assets_dir: str | os.PathLike | None = None,
theme: rio.Theme | tuple[rio.Theme, rio.Theme] | None = None,
build_connection_lost_message: Callable[
build_connection_lost_message: t.Callable[
[], rio.Component
] = make_default_connection_lost_component,
meta_tags: dict[str, str] = {},
@@ -253,10 +253,13 @@ class App:
media sites to display information about your page, such as the
title and a short description.
"""
self._main_file = _get_main_file()
if name is None:
name = _get_default_app_name(self._main_file)
# A common mistake is to pass types instead of instances to
# `default_attachments`. Catch that, scream and die.
for attachment in default_attachments:
if isinstance(attachment, type):
raise TypeError(
f"Default attachments should be instances, not types. Did you mean to type `{attachment.__name__}()`?"
)
if description is None:
description = "A Rio web-app written in 100% Python"
@@ -270,36 +273,32 @@ class App:
if theme is None:
theme = rio.Theme.from_colors()
# A common mistake is to pass types instead of instances to
# `default_attachments`. Catch that, scream and die.
for attachment in default_attachments:
if isinstance(attachment, type):
raise TypeError(
f"Default attachments should be instances, not types. Did you mean to type `{attachment.__name__}()`?"
)
if name is None:
name = self._infer_app_name()
# The `main_file` isn't detected correctly if the app is launched via
# `rio run`. We'll store the user input so that `rio run` can fix the
# assets dir.
self._assets_dir = assets_dir
self._compute_assets_dir()
if assets_dir is None:
assets_dir = self._infer_assets_dir()
else:
assets_dir = Path(assets_dir)
# Similarly, we can't auto-detect the pages until we know where the
# user's project is located.
self._raw_pages = pages
self.pages = ()
if pages is None:
pages = self._infer_pages()
elif isinstance(pages, (os.PathLike, str)):
pages = routing.auto_detect_pages(Path(pages))
else:
pages = list(pages)
self.name = name
self.description = description
self.assets_dir = assets_dir
self.pages = pages
self._build = build
self._icon = assets.Asset.from_image(icon)
self._on_app_start = on_app_start
self._on_app_close = on_app_close
self._on_session_start = on_session_start
self._on_session_close = on_session_close
self.default_attachments: MutableSequence[Any] = list(
default_attachments
)
self.default_attachments = list(default_attachments)
self._theme = theme
self._build_connection_lost_message = build_connection_lost_message
self._custom_meta_tags = meta_tags
@@ -309,37 +308,172 @@ class App:
else:
self._ping_pong_interval = timedelta(seconds=ping_pong_interval)
@property
def _module_path(self) -> Path:
if utils.is_python_script(self._main_file):
return self._main_file.parent
def _infer_app_name(self) -> str:
main_file_path = self._main_file_path
name = main_file_path.stem
if name in ("main", "__main__", "__init__"):
name = main_file_path.absolute().parent.name
return name.replace("_", " ").title()
def _infer_assets_dir(self) -> Path:
# If the "main_file" is a sub-module, then it's unclear where the assets
# directory would be located - it could be a sibling of the main file,
# or it could be at the root of the package hierarchy. I don't want to
# enforce a specific location, so we'll just loop through all the
# packages and see if any of them contain an "assets" folder.
for directory in self._packages_in_project:
assets_dir = directory / "assets"
if assets_dir.is_dir():
return assets_dir
# No luck in any of the package folders? Try the project folder as well
assets_dir = self._project_dir / "assets"
if assets_dir.is_dir():
return assets_dir
# No "assets" folder in the project directory? Then just use the project
# directory itself.
return self._project_dir
def _infer_pages(self) -> list[rio.ComponentPage]:
# Similar to the assets_dir, we don't want to enforce a specific
# location for the `pages` folder. Scan all the packages in the project.
#
# As a failsafe, we also allow a `pages` package directly in the project
# directory.
for directory in (*self._packages_in_project, self._project_dir):
pages_dir = directory / "pages"
if not pages_dir.exists():
continue
# Now we know the location of the `pages` folder, but in order to
# import it correctly we must also know its module name.
location_in_package = pages_dir.relative_to(self._project_dir)
module_name = ".".join(location_in_package.parts)
return routing.auto_detect_pages(pages_dir, package=module_name)
# No `pages` folder found? No pages, then.
# TODO: Throw an error? Display a warning?
return []
@functools.cached_property
def _main_file_path(self) -> Path:
if global_state.rio_run_app_module_path is not None:
main_file = global_state.rio_run_app_module_path
else:
return self._main_file
try:
main_file = Path(__main__.__file__)
except AttributeError:
main_file = Path(sys.argv[0])
def _compute_assets_dir(self) -> None:
self.assets_dir = self._module_path / self._assets_dir
# Find out if we're being executed by uvicorn
if (
main_file.name != "__main__.py"
or main_file.parent != Path(uvicorn.__file__).parent
):
return main_file
def _load_pages(self) -> None:
pages: Iterable[rio.ComponentPage | rio.Redirect]
# Find out from which module uvicorn imported the app
try:
app_location = next(arg for arg in sys.argv[1:] if ":" in arg)
except StopIteration:
return main_file
if self._raw_pages is None:
pages = routing.auto_detect_pages(
self._module_path / "pages",
package=f"{self._module_path.stem}.pages",
)
elif isinstance(self._raw_pages, (os.PathLike, str)):
pages = routing.auto_detect_pages(Path(self._raw_pages))
module_name, _, _ = app_location.partition(":")
module = sys.modules[module_name]
if module.__file__ is not None:
return Path(module.__file__)
return main_file
@functools.cached_property
def _packages_in_project(self) -> tuple[Path, ...]:
"""
Returns a list of all package directories from the "main_file" up to the
topmost package that contains it.
"""
result = list[Path]()
if utils.is_python_script(self._main_file_path):
package_path = self._main_file_path.parent
else:
pages = self._raw_pages # type: ignore (wtf?)
package_path = self._main_file_path
self.pages = tuple(pages)
# The "main file" might be a sub-module. Try to find the package root.
while True:
result.append(package_path)
parent_dir = package_path.parent
# If we've reached the root of the file system, stop looping
if parent_dir == package_path:
break
# If the parent folder contains an `__init__.py` file, go up another
# level
if not (parent_dir / "__init__.py").is_file():
break
package_path = parent_dir
return tuple(result)
@functools.cached_property
def _package_root_path(self) -> Path:
"""
This is the path to the project's topmost package directory, or if no
package exists, the directory that contains the "main file".
"""
if utils.is_python_script(self._main_file_path):
package_path = self._main_file_path.parent
else:
package_path = self._main_file_path
# The "main file" might be a sub-module. Try to find the package root.
while True:
parent_dir = package_path.parent
# If we've reached the root of the file system, stop looping
if parent_dir == package_path:
break
# If the parent folder contains an `__init__.py` file, go up another
# level
if not (parent_dir / "__init__.py").is_file():
break
package_path = parent_dir
return package_path
@functools.cached_property
def _project_dir(self) -> Path:
# Careful: `self._package_root_path` may or may not be a package. If
# it's not a package, then it's actually the project directory (or
# `src`).
if (self._package_root_path / "__init__.py").is_file():
project_dir = self._package_root_path.parent
else:
project_dir = self._package_root_path
if project_dir.name == "src":
project_dir = project_dir.parent
return project_dir
def _as_fastapi(
self,
*,
debug_mode: bool,
running_in_window: bool,
internal_on_app_start: Callable[[], Any] | None,
internal_on_app_start: t.Callable[[], t.Any] | None,
base_url: rio.URL | str | None,
) -> fastapi.FastAPI:
"""
@@ -355,9 +489,6 @@ class App:
if isinstance(base_url, str):
base_url = rio.URL(base_url)
# We're starting! We can't delay loading the pages any longer.
self._load_pages()
# Build the fastapi instance
return fastapi_server.FastapiServer(
self,
@@ -418,8 +549,8 @@ class App:
port: int,
quiet: bool,
running_in_window: bool,
internal_on_app_start: Callable[[], None] | None = None,
internal_on_server_created: Callable[[uvicorn.Server], None]
internal_on_app_start: t.Callable[[], None] | None = None,
internal_on_server_created: t.Callable[[uvicorn.Server], None]
| None = None,
base_url: rio.URL | str | None = None,
) -> None:
@@ -693,46 +824,10 @@ pixels_per_rem
)
finally:
server = cast(
server = t.cast(
uvicorn.Server, server
) # Prevents "unreachable code" warning
assert isinstance(server, uvicorn.Server)
server.should_exit = True
server_thread.join()
def _get_main_file() -> Path:
try:
main_file = Path(__main__.__file__)
except AttributeError:
main_file = Path(sys.argv[0])
# Find out if we're being executed by uvicorn
if (
main_file.name != "__main__.py"
or main_file.parent != Path(uvicorn.__file__).parent
):
return main_file
# Find out from which module uvicorn imported the app
try:
app_location = next(arg for arg in sys.argv[1:] if ":" in arg)
except StopIteration:
return main_file
module_name, _, _ = app_location.partition(":")
module = sys.modules[module_name]
if module.__file__ is None:
return main_file
return Path(module.__file__)
def _get_default_app_name(main_file: Path) -> str:
name = main_file.stem
if name in ("main", "__main__", "__init__"):
name = main_file.absolute().parent.stem
return name.replace("_", " ").title()

View File

@@ -11,7 +11,6 @@ import warnings
import weakref
from datetime import date
from pathlib import Path
from typing import *
import langcodes
import pytz

View File

@@ -8,10 +8,11 @@ import io
import json
import logging
import secrets
import typing as t
import warnings
import weakref
from datetime import timedelta
from pathlib import Path
from typing import * # type: ignore
from xml.etree import ElementTree as ET
import crawlerdetect
@@ -48,7 +49,7 @@ __all__ = [
]
P = ParamSpec("P")
P = t.ParamSpec("P")
# Used to identify search engine crawlers (like googlebot) and serve them
@@ -59,9 +60,7 @@ CRAWLER_DETECTOR = crawlerdetect.CrawlerDetect()
@functools.lru_cache(maxsize=None)
def _build_sitemap(base_url: rio.URL, app: rio.App) -> str:
# Find all pages to add
page_urls = {
rio.URL(""),
}
page_urls = {rio.URL("")}
def worker(
parent_url: rio.URL,
@@ -110,8 +109,8 @@ def read_frontend_template(template_name: str) -> str:
def add_cache_headers(
func: Callable[P, Awaitable[fastapi.Response]],
) -> Callable[P, Coroutine[None, None, fastapi.Response]]:
func: t.Callable[P, t.Awaitable[fastapi.Response]],
) -> t.Callable[P, t.Coroutine[None, None, fastapi.Response]]:
"""
Decorator for routes that serve static files. Ensures that the response has
the `Cache-Control` header set appropriately.
@@ -181,7 +180,7 @@ class FastapiServer(fastapi.FastAPI, AbstractAppServer):
app_: app.App,
debug_mode: bool,
running_in_window: bool,
internal_on_app_start: Callable[[], None] | None,
internal_on_app_start: t.Callable[[], None] | None,
base_url: rio.URL | None,
) -> None:
super().__init__(
@@ -327,15 +326,26 @@ class FastapiServer(fastapi.FastAPI, AbstractAppServer):
# The route that serves the index.html will be registered later, so that
# it has a lower priority than user-created routes.
#
# This keeps track of whether the fallback route has already been
# registered.
self._index_hmtl_route_registered = False
async def __call__(self, scope, receive, send) -> None:
# Because this is a single page application, all other routes should
# serve the index page. The session will determine which components
# should be shown.
self.add_api_route(
"/{initial_route_str:path}", self._serve_index, methods=["GET"]
)
#
# This route is registered last, so that it has the lowest priority.
# This allows the user to add custom routes that take precedence.
if not self._index_hmtl_route_registered:
self._index_hmtl_route_registered = True
self.add_api_route(
"/{initial_route_str:path}", self._serve_index, methods=["GET"]
)
# Delegate to FastAPI
return await super().__call__(scope, receive, send)
@contextlib.asynccontextmanager
@@ -526,6 +536,24 @@ class FastapiServer(fastapi.FastAPI, AbstractAppServer):
html_base_url,
)
theme = self.app._theme
if isinstance(theme, tuple):
light_theme_background_color = theme[0].background_color
dark_theme_background_color = theme[1].background_color
else:
light_theme_background_color = theme.background_color
dark_theme_background_color = theme.background_color
html_ = html_.replace(
"{light_theme_background_color}",
f"#{light_theme_background_color.hex}",
)
html_ = html_.replace(
"{dark_theme_background_color}",
f"#{dark_theme_background_color.hex}",
)
# Since the title is user-defined, it might contain placeholders like
# `{debug_mode}`. So it's important that user-defined content is
# inserted last.
@@ -604,6 +632,17 @@ Sitemap: {base_url / "/rio/sitemap"}
image.save(output_buffer, format="png")
except Exception as err:
if isinstance(self.app._icon, assets.PathAsset):
warnings.warn(
f"Could not fetch the app's icon from {self.app._icon.path.resolve()}"
)
elif isinstance(self.app._icon, assets.UrlAsset):
warnings.warn(
f"Could not fetch the app's icon from {self.app._icon.url}"
)
else:
warnings.warn(f"Could not fetch the app's icon from")
raise fastapi.HTTPException(
status_code=fastapi.status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Could not fetch the app's icon.",

View File

@@ -1,7 +1,5 @@
from __future__ import annotations
from typing import *
import rio
from .. import assets, utils

View File

@@ -4,12 +4,12 @@ import abc
import hashlib
import io
import os
import typing as t
from pathlib import Path
from typing import * # type: ignore
import httpx
import typing_extensions as te
from PIL.Image import Image
from typing_extensions import Self
from yarl import URL
import rio
@@ -65,15 +65,15 @@ class Asset(SelfSerializing):
# The MIME type of the asset
self.media_type = media_type
@overload
@t.overload
@classmethod
def new(cls, data: bytes, media_type: str | None = None) -> BytesAsset: ...
@overload
@t.overload
@classmethod
def new(cls, data: Path, media_type: str | None = None) -> PathAsset: ...
@overload
@t.overload
@classmethod
def new(cls, data: URL, media_type: str | None = None) -> UrlAsset: ...
@@ -150,7 +150,7 @@ class Asset(SelfSerializing):
return self._eq(other)
@abc.abstractmethod
def _eq(self, other: Self) -> bool:
def _eq(self, other: te.Self) -> bool:
raise NotImplementedError
@abc.abstractmethod
@@ -191,7 +191,7 @@ class HostedAsset(Asset):
def __hash__(self) -> int:
return hash(self.secret_id)
def _eq(self, other: Self) -> bool:
def _eq(self, other: te.Self) -> bool:
return self.secret_id == other.secret_id
def _serialize(self, sess: rio.Session) -> str:
@@ -277,7 +277,7 @@ class UrlAsset(Asset):
def __hash__(self) -> int:
return hash(self._url)
def _eq(self, other: Self) -> bool:
def _eq(self, other: te.Self) -> bool:
return self._url == other._url
def _serialize(self, sess: rio.Session) -> str:

View File

@@ -6,8 +6,9 @@ https://github.com/tiangolo/fastapi/issues/1240#issuecomment-1055396884
"""
import mimetypes
import typing as t
import warnings
from pathlib import Path
from typing import * # type: ignore
import fastapi
from fastapi import HTTPException
@@ -18,11 +19,11 @@ __all__ = [
def send_bytes_range_requests(
file_obj: BinaryIO,
file_obj: t.BinaryIO,
start: int,
end: int,
chunk_size: int = 16 * 1024 * 1024,
) -> Iterator[bytes]:
) -> t.Iterator[bytes]:
"""
Send a file in chunks using Range Requests specification RFC7233. `start`
and `end` are inclusive as per the spec.
@@ -71,13 +72,15 @@ def range_requests_response(
Returns a fastapi response which serves the given file, supporting Range
Requests as per RFC7233 ("HTTP byte serving").
Returns a 404 if the file does not exist.
Returns a 404 if the file does not exist. In this case a warning is also
shown in the console.
"""
# Get the file size. This also verifies the file exists.
try:
file_size_in_bytes = file_path.stat().st_size
except FileNotFoundError:
warnings.warn(f"Cannot find file at {file_path.resolve()}")
return fastapi.responses.Response(status_code=404)
# Prepare response headers
@@ -91,7 +94,16 @@ def range_requests_response(
}
if media_type is None:
media_type = mimetypes.guess_type(file_path, strict=False)[0]
# There have been issues with javascript files because browsers insist
# on the mime type "text/javascript", but some PCs aren't configured
# correctly and return "text/plain". So we purposely avoid using
# `mimetypes.guess_type` for javascript files.
suffixes = file_path.suffixes
if suffixes and suffixes[0] == ".js":
media_type = "text/javascript"
else:
media_type = mimetypes.guess_type(file_path, strict=False)[0]
if media_type is not None:
headers["content-type"] = media_type

View File

@@ -5,8 +5,8 @@ from .. import project_config
_logger = logging.getLogger(__name__)
import typing as t
from pathlib import Path
from typing import Literal
import introspection
import revel
@@ -67,7 +67,7 @@ def new(
nicename: str,
*,
# Website is listed first to make it the default
type: Literal["website", "app"],
type: t.Literal["website", "app"],
template: rio.snippets.AvailableTemplatesLiteral,
) -> None:
project_setup.create_project(
@@ -213,9 +213,13 @@ containing some template code will be created in the `pages` or `components`
folder of your project.
""",
)
def add(what: Literal["page", "component"], /, name: str) -> None:
def add(what: t.Literal["page", "component"], /, name: str) -> None:
with project_config.RioProjectConfig.try_locate_and_load() as proj:
module_path = proj.app_main_module_path
try:
module_path = proj.app_main_module_path
except FileNotFoundError as error:
fatal(str(error))
if not module_path.is_dir():
fatal(
f"Cannot add {what}s to a single-file project. Please convert"
@@ -241,7 +245,7 @@ def add(what: Literal["page", "component"], /, name: str) -> None:
file_path.write_text(
f"""from __future__ import annotations
from typing import * # type: ignore
import typing as t
import rio
@@ -269,7 +273,7 @@ culpa qui officia deserunt mollit anim id est laborum.
f"""from __future__ import annotations
from dataclasses import KW_ONLY, field
from typing import * # type: ignore
import typing as t
import rio

View File

@@ -1,7 +1,6 @@
from __future__ import annotations
from pathlib import Path
from typing import * # type: ignore
import keyring
import platformdirs

View File

@@ -8,8 +8,10 @@ import io
import linecache
import sys
import traceback
import typing as t
from pathlib import Path
from typing import Callable, IO, Optional
import revel
from dataclasses import dataclass
@@ -52,12 +54,12 @@ def _handle_syntax_error(err: SyntaxError) -> traceback.FrameSummary:
)
def _format_single_exception_raw(
out: IO[str],
out: t.IO[str],
err: BaseException,
*,
include_header: bool,
style: FormatStyle,
relpath: Optional[Path],
relpath: Path | None,
frame_filter: Callable[[traceback.FrameSummary], bool],
) -> None:
"""
@@ -115,8 +117,8 @@ def format_exception_raw(
err: BaseException,
*,
style: FormatStyle,
relpath: Optional[Path] = None,
frame_filter: Optional[Callable[[traceback.FrameSummary], bool]] = None,
relpath: Path | None = None,
frame_filter: t.Callable[[traceback.FrameSummary], bool] = lambda _: True,
) -> str:
"""
Format an exception into a pretty string with the given style.
@@ -155,8 +157,8 @@ def format_exception_raw(
def format_exception_revel(
err: BaseException,
*,
relpath: Optional[Path] = None,
frame_filter: Optional[Callable[[traceback.FrameSummary], bool]] = None,
relpath: Path | None = None,
frame_filter: t.Callable[[traceback.FrameSummary], bool] = lambda _: True,
) -> str:
"""
Format an exception using Revel's styling.
@@ -183,8 +185,8 @@ def format_exception_revel(
def format_exception_html(
err: BaseException,
*,
relpath: Optional[Path] = None,
frame_filter: Optional[Callable[[traceback.FrameSummary], bool]] = None,
relpath: Path | None = None,
frame_filter: t.Callable[[traceback.FrameSummary], bool] = lambda _: True,
) -> str:
"""
Format an exception into HTML with appropriate styling.

View File

@@ -2,8 +2,8 @@ import io
import re
import shutil
import string
import typing as t
from pathlib import Path
from typing import * # type: ignore
import introspection
import isort
@@ -32,7 +32,9 @@ def class_name_from_snippet(snip: rio.snippets.Snippet) -> str:
return "".join(part.capitalize() for part in parts)
def write_init_file(fil: IO, snippets: Iterable[rio.snippets.Snippet]) -> None:
def write_init_file(
fil: t.IO, snippets: t.Iterable[rio.snippets.Snippet]
) -> None:
"""
Write an `__init__.py` file that imports all of the snippets.
@@ -51,10 +53,10 @@ def write_init_file(fil: IO, snippets: Iterable[rio.snippets.Snippet]) -> None:
def generate_root_init(
out: TextIO,
out: t.TextIO,
*,
raw_name: str,
project_type: Literal["app", "website"],
project_type: t.Literal["app", "website"],
template: rio.snippets.ProjectTemplate,
) -> None:
"""
@@ -77,7 +79,7 @@ def generate_root_init(
from __future__ import annotations
from pathlib import Path
from typing import * # type: ignore
import typing as t
import rio
@@ -218,7 +220,7 @@ def derive_module_name(raw_name: str) -> str:
def generate_readme(
out: TextIO,
out: t.TextIO,
raw_name: str,
template: rio.snippets.ProjectTemplate,
) -> None:
@@ -247,7 +249,7 @@ This project is based on the `{template.name}` template.
def write_component_file(
out: TextIO,
out: t.TextIO,
snip: rio.snippets.Snippet,
import_depth: int,
) -> None:
@@ -263,7 +265,7 @@ def write_component_file(
f"""from __future__ import annotations
from dataclasses import KW_ONLY, field
from typing import * # type: ignore
import typing as t
import rio
@@ -314,7 +316,7 @@ def generate_dependencies_file(
def create_project(
*,
raw_name: str,
type: Literal["app", "website"],
type: t.Literal["app", "website"],
template_name: rio.snippets.AvailableTemplatesLiteral,
target_parent_directory: Path,
) -> None:

View File

@@ -1,5 +1,5 @@
import typing as t
from datetime import timedelta
from typing import * # type: ignore
import httpx
@@ -46,7 +46,7 @@ class RioApi:
async def __aenter__(self) -> "RioApi":
return self
async def __aexit__(self, *args: Any) -> None:
async def __aexit__(self, *args: t.Any) -> None:
await self.close()
async def close(self) -> None:
@@ -65,10 +65,10 @@ class RioApi:
self,
endpoint: str,
*,
method: Literal["get", "post", "delete"] = "get",
json: dict[str, Any] | None = None,
file: BinaryIO | None = None,
) -> Any:
method: t.Literal["get", "post", "delete"] = "get",
json: dict[str, t.Any] | None = None,
file: t.BinaryIO | None = None,
) -> t.Any:
"""
Make a request to the Rio API.
"""
@@ -137,7 +137,7 @@ class RioApi:
await self.request("/auth/expireToken", method="post")
async def get_user(self) -> dict[str, Any]:
async def get_user(self) -> dict[str, t.Any]:
"""
Return the user's information, if logged in.
"""
@@ -148,8 +148,8 @@ class RioApi:
self,
*,
name: str,
packed_app: BinaryIO,
realm: Literal["pro", "free", "test"],
packed_app: t.BinaryIO,
realm: t.Literal["pro", "free", "test"],
start: bool,
) -> None:
assert self.is_logged_in, "Must be logged in to create/update an app"

View File

@@ -1,16 +1,18 @@
import functools
import html
import importlib
import os
import sys
import traceback
import types
import typing as t
from pathlib import Path
from typing import * # type: ignore
import path_imports
import revel
import rio
import rio.global_state
import rio.app_server.fastapi_server
from rio import icon_registry
from ... import project_config
@@ -32,7 +34,7 @@ def traceback_frame_filter(frame: traceback.FrameSummary) -> bool:
def make_traceback_html(
*,
err: Union[str, BaseException],
err: t.Union[str, BaseException],
project_directory: Path,
) -> str:
error_icon_svg = icon_registry.get_icon_svg("material/error")
@@ -70,7 +72,7 @@ def make_traceback_html(
def make_error_message_component(
err: Union[str, BaseException],
err: t.Union[str, BaseException],
project_directory: Path,
) -> rio.Component:
html = make_traceback_html(
@@ -86,9 +88,9 @@ def make_error_message_component(
def make_error_message_app(
err: Union[str, BaseException],
err: t.Union[str, BaseException],
project_directory: Path,
theme: rio.Theme | Tuple[rio.Theme, rio.Theme],
theme: rio.Theme | tuple[rio.Theme, rio.Theme],
) -> rio.App:
"""
Creates an app that displays the given error message.
@@ -101,6 +103,77 @@ def make_error_message_app(
)
def modules_in_directory(project_path: Path) -> t.Iterable[str]:
"""
Returns all currently loaded modules that reside in the given directory (or
any subdirectory thereof). As a second condition, modules located inside of
a virtual environment are not returned.
The purpose of this is to yield all modules that belong to the user's
project. This is the set of modules that makes sense to reload when the user
makes a change.
"""
# Resolve the path to avoid issues with symlinks
project_path = project_path.resolve()
# Paths known to be virtual environments, or not. All contained paths are
# absolute.
#
# This acts as a cache to avoid hammering the filesystem.
virtual_environment_paths: dict[Path, bool] = {}
def is_virtualenv_dir(path: Path) -> bool:
# Resolve the path to make sure we're dealing in absolutes and to avoid
# issues with symlinks
path = path.resolve()
# Cached?
try:
return virtual_environment_paths[path]
except KeyError:
pass
# Nope. Is this a venv?
result = (path / "pyvenv.cfg").exists()
# Cache & return
virtual_environment_paths[path] = result
return result
# Walk all modules
for name, module in list(sys.modules.items()):
# Special case: Unloading Rio, while Rio is running is not that smart.
if name == "rio" or name.startswith("rio."):
continue
# Where does the module live?
try:
module_path = getattr(module, "__file__", None)
except AttributeError:
continue
try:
module_path = Path(module_path).resolve() # type: ignore
except TypeError:
continue
# If the module isn't inside of the project directory, skip it
if not module_path.is_relative_to(project_path):
continue
# Check all parent directories for virtual environments, up to the
# project directory
for parent in module_path.parents:
# If we've reached the project directory, stop
if parent == project_path:
yield name
break
# If this is a virtual environment, skip the module
if is_virtualenv_dir(parent):
break
def import_app_module(
proj: project_config.RioProjectConfig,
) -> types.ModuleType:
@@ -108,30 +181,42 @@ def import_app_module(
Python's importing is bizarre. This function tries to hide all of that and
imports the module, as specified by the user. This can raise a variety of
exceptions, since the module's code is evaluated.
The module will be freshly imported, even if it was already imported before.
"""
# Purge the module from the module cache
app_main_module = proj.app_main_module
root_module, _, _ = app_main_module.partition(".")
# Purge all modules that belong to the project. While the main module name
# is known, deleting only that isn't enough in all projects. In complex
# project structures it can be useful to have the UI code live in a module
# that is then just loaded into a top-level Python file.
for module_name in modules_in_directory(proj.project_directory):
del sys.modules[module_name]
for module_name in list(sys.modules):
if module_name.partition(".")[0] == root_module:
del sys.modules[module_name]
# Explicitly tell the app what the "main file" is, because otherwise it
# would be detected incorrectly.
rio.global_state.rio_run_app_module_path = proj.app_main_module_path
# Inject the module path into `sys.path`. We add it at the start so that it
# takes priority over all other modules. (Example: If someone names their
# project "test", we don't end up importing python's builtin `test` module
# on accident.)
main_module_path = str(proj.app_main_module_path.parent)
sys.path.insert(0, main_module_path)
# Now (re-)import the app module
# Now (re-)import the app module. There is no need to import all the other
# modules here, since they'll be re-imported as needed by the app module.
try:
return importlib.import_module(app_main_module)
return path_imports.import_from_path(
proj.app_main_module_path,
proj.app_main_module,
import_parent_modules=True,
# Newbies often don't organize their code as a single module, so to
# guarantee that all their files can be imported, we'll add the
# relevant directory to `sys.path`
add_parent_directory_to_sys_path=True,
)
finally:
sys.path.remove(main_module_path)
rio.global_state.rio_run_app_module_path = None
def load_user_app(proj: project_config.RioProjectConfig) -> rio.App:
def load_user_app(
proj: project_config.RioProjectConfig,
) -> tuple[
rio.App,
rio.app_server.fastapi_server.FastapiServer | None,
]:
"""
Load and return the user app. Raises `AppLoadError` if the app can't be
loaded for whichever reason.
@@ -152,41 +237,58 @@ def load_user_app(proj: project_config.RioProjectConfig) -> rio.App:
raise AppLoadError() from err
# Find the variable holding the Rio app
apps: list[tuple[str, rio.App]] = []
for var_name, var in app_module.__dict__.items():
if isinstance(var, rio.App):
apps.append((var_name, var))
# Find the variable holding the Rio app.
#
# There are two cases here. Typically, there will be an instance of
# `rio.App` somewhere. However, in order for users to be able to add custom
# routes, there might also be a variable storing a `fastapi.FastAPI`, or, in
# our case, an Rio's subclass thereof. If that is present, prefer it over
# the plain Rio app.
as_fastapi_apps: list[
tuple[str, rio.app_server.fastapi_server.FastapiServer]
] = []
rio_apps: list[tuple[str, rio.App]] = []
for var_name, var in app_module.__dict__.items():
if isinstance(var, rio.app_server.fastapi_server.FastapiServer):
as_fastapi_apps.append((var_name, var))
elif isinstance(var, rio.App):
rio_apps.append((var_name, var))
# Prepare the main file name
if app_module.__file__ is None:
main_file_reference = f"Your app's main file"
else:
main_file_reference = f"The file `{Path(app_module.__file__).relative_to(proj.project_directory)}`"
if len(apps) == 0:
# Which type of app do we have?
#
# Case: FastAPI app
if len(as_fastapi_apps) > 0:
app_list = as_fastapi_apps
app_server = as_fastapi_apps[0][1]
app_instance = app_server.app
# Case: Rio app
elif len(rio_apps) > 0:
app_list = rio_apps
app_server = None
app_instance = rio_apps[0][1]
# Case: No app
else:
raise AppLoadError(
f"Cannot find your app. {main_file_reference} needs to to define a"
f" variable that is a Rio app. Something like `app = rio.App(...)`"
)
if len(apps) > 1:
# Make sure there was only one app to choose from, within the chosen
# category
if len(app_list) > 1:
variables_string = (
"`" + "`, `".join(var_name for var_name, _ in apps) + "`"
"`" + "`, `".join(var_name for var_name, _ in app_list) + "`"
)
raise AppLoadError(
f"{main_file_reference} defines multiple Rio apps: {variables_string}. Please make sure there is exactly one."
)
app = apps[0][1]
# Explicitly set the project location because it can't reliably be
# auto-detected. This also affects the assets_dir and the implicit page
# loading.
app._main_file = proj.app_main_module_path
app._compute_assets_dir()
app._load_pages()
app._raw_pages = app.pages # Prevent auto_detect_pages() from running twice
return app
return app_instance, app_server

View File

@@ -6,9 +6,9 @@ import socket
import sys
import threading
import time
import typing as t
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import * # type: ignore
import httpx
import revel
@@ -32,7 +32,7 @@ from . import (
try:
import webview # type: ignore
except ImportError:
if TYPE_CHECKING:
if t.TYPE_CHECKING:
import webview # type: ignore
else:
webview = None
@@ -100,9 +100,10 @@ class Arbiter:
# The app to use for creating apps. This keeps the theme consistent if
# for-example the user's app crashes and then a mock-app is injected.
self._app_theme: Union[rio.Theme, tuple[rio.Theme, rio.Theme]] = (
rio.Theme.pair_from_colors()
)
self._app_theme: t.Union[
rio.Theme,
tuple[rio.Theme, rio.Theme],
] = rio.Theme.pair_from_colors()
# Prefer to consistently run on the same port, as that makes it easier
# to connect to - this way old browser tabs don't get invalidated
@@ -166,7 +167,7 @@ class Arbiter:
return f"http://{local_ip}:{self.port}"
@property
def running_tasks(self) -> Iterator[asyncio.Task[None]]:
def running_tasks(self) -> t.Iterator[asyncio.Task[None]]:
for task in (
self._uvicorn_task,
self._file_watcher_task,
@@ -288,21 +289,32 @@ class Arbiter:
)
self._webview_worker.request_stop()
def try_load_app(self) -> tuple[rio.App, Exception | None]:
def try_load_app(
self,
) -> tuple[
rio.App,
rio.app_server.fastapi_server.FastapiServer | None,
Exception | None,
]:
"""
Tries to load the user's app. If it fails, a dummy app is created and
returned, unless running in release mode.
Returns the new app and the error that occurred, if any.
Returns the app instance, the app's server instance and an exception if
the app could not be loaded.
The app server is returned in case the user has called `as_fastapi` on
their app instance. In that case the actual fastapi app should be
hosted, so any custom routes take effect.
"""
rio.cli._logger.debug("Trying to load the app")
try:
app = app_loading.load_user_app(self.proj)
app, app_server = app_loading.load_user_app(self.proj)
except app_loading.AppLoadError as err:
if err.__cause__ is not None:
err = cast(Exception, err.__cause__)
err = t.cast(Exception, err.__cause__)
# Announce the problem in the terminal
rio.cli._logger.critical(f"The app could not be loaded: {err}")
@@ -321,6 +333,7 @@ class Arbiter:
self.proj.project_directory,
self._app_theme,
),
None,
err,
)
@@ -328,7 +341,7 @@ class Arbiter:
# this theme will be used for it.
self._app_theme = app._theme
return app, None
return app, app_server, None
def run(self) -> None:
assert not self._stop_requested.is_set()
@@ -504,7 +517,7 @@ class Arbiter:
apply_monkeypatches()
# Try to load the app
app, _ = self.try_load_app()
app, app_server, _ = self.try_load_app()
# Start the file watcher
if self.debug_mode:
@@ -527,6 +540,7 @@ class Arbiter:
self._uvicorn_worker = uvicorn_worker.UvicornWorker(
push_event=self.push_event,
app=app,
app_server=app_server,
socket=sock,
quiet=self.quiet,
debug_mode=self.debug_mode,
@@ -668,7 +682,7 @@ class Arbiter:
else:
raise NotImplementedError(f'Unknown event "{event}"')
def _spawn_traceback_popups(self, err: Union[str, BaseException]) -> None:
def _spawn_traceback_popups(self, err: t.Union[str, BaseException]) -> None:
"""
Displays a popup with the traceback in the rio UI.
"""
@@ -730,10 +744,10 @@ window.setConnectionLostPopupVisible(true);
await app_server._call_on_app_close()
# Load the user's app again
new_app, loading_error = self.try_load_app()
new_app, new_app_server, loading_error = self.try_load_app()
# Replace the app which is currently hosted by uvicorn
self._uvicorn_worker.replace_app(new_app)
self._uvicorn_worker.replace_app(new_app, new_app_server)
# The app has changed, but the uvicorn server is still the same.
# Because of this, uvicorn won't call the `on_app_start` function -

View File

@@ -1,6 +1,6 @@
import time
import typing as t
from pathlib import Path
from typing import * # type: ignore
import watchfiles
@@ -12,7 +12,7 @@ class FileWatcherWorker:
def __init__(
self,
*,
push_event: Callable[[run_models.Event], None],
push_event: t.Callable[[run_models.Event], None],
proj: project_config.RioProjectConfig,
) -> None:
self.push_event = push_event

View File

@@ -1,12 +1,12 @@
import threading
from typing import * # type: ignore
import typing as t
T = TypeVar("T")
T = t.TypeVar("T")
class ThreadsafeFuture(Generic[T]):
class ThreadsafeFuture(t.Generic[T]):
def __init__(self) -> None:
self._result_value: Any
self._result_value: t.Any
self._event = threading.Event()
def set_result(self, result: T) -> None:

View File

@@ -1,15 +1,17 @@
import asyncio
import socket
from typing import * # type: ignore
import typing as t
import revel
import uvicorn
import uvicorn.lifespan.on
from starlette.types import Receive, Scope, Send
import rio
import rio.app_server.fastapi_server
import rio.cli
from ... import utils
from .. import nice_traceback
from . import run_models
@@ -18,8 +20,9 @@ class UvicornWorker:
def __init__(
self,
*,
push_event: Callable[[run_models.Event], None],
push_event: t.Callable[[run_models.Event], None],
app: rio.App,
app_server: rio.app_server.fastapi_server.FastapiServer | None,
socket: socket.socket,
quiet: bool,
debug_mode: bool,
@@ -28,7 +31,6 @@ class UvicornWorker:
base_url: rio.URL | None,
) -> None:
self.push_event = push_event
self.app = app
self.socket = socket
self.quiet = quiet
self.debug_mode = debug_mode
@@ -36,15 +38,19 @@ class UvicornWorker:
self.on_server_is_ready_or_failed = on_server_is_ready_or_failed
self.base_url = base_url
# The app server used to host the app
# The app server used to host the app.
#
# This can optionally be provided to the constructor. If not, it will be
# created when the worker is started. This allows for the app to be
# either a Rio app or a FastAPI app (derived from a Rio app).
self.app = app
self.app_server: rio.app_server.fastapi_server.FastapiServer | None = (
None
)
async def run(self) -> None:
rio.cli._logger.debug("Uvicorn worker is starting")
self.replace_app(app, app_server)
# Set up a uvicorn server, but don't start it yet
def _create_and_store_app_server(self) -> None:
app_server = self.app._as_fastapi(
debug_mode=self.debug_mode,
running_in_window=self.run_in_window,
@@ -58,8 +64,33 @@ class UvicornWorker:
)
self.app_server = app_server
async def run(self) -> None:
rio.cli._logger.debug("Uvicorn worker is starting")
# Create the app server
if self.app_server is None:
self._create_and_store_app_server()
assert self.app_server is not None
# Instead of using the ASGI app directly, create a transparent shim that
# redirect's to the worker's currently stored app server. This allows
# replacing the app server at will because the shim always remains the
# same.
#
# ASGI is a bitch about function signatures. This function cannot be a
# simple method, because the added `self` parameter seems to confused
# whoever the caller is. Hence the nested function.
async def _asgi_shim(
scope: Scope,
receive: Receive,
send: Send,
) -> None:
assert self.app_server is not None
await self.app_server(scope, receive, send)
# Set up a uvicorn server, but don't start it yet
config = uvicorn.Config(
self.app_server,
app=_asgi_shim,
log_config=None, # Prevent uvicorn from configuring global logging
log_level="error" if self.quiet else "info",
timeout_graceful_shutdown=1, # Without a timeout the server sometimes deadlocks
@@ -84,7 +115,7 @@ class UvicornWorker:
# output in the console. This monkeypatch suppresses that.
original_receive = uvicorn.lifespan.on.LifespanOn.receive
async def patched_receive(self) -> Any:
async def patched_receive(self) -> t.Any:
try:
return await original_receive(self)
except asyncio.CancelledError:
@@ -120,11 +151,39 @@ class UvicornWorker:
finally:
rio.cli._logger.debug("Uvicorn serve task has ended")
def replace_app(self, app: rio.App) -> None:
def replace_app(
self,
app: rio.App,
app_server: rio.app_server.fastapi_server.FastapiServer | None,
) -> None:
"""
Replace the app currently running in the server with a new one. The
worker must already be running for this to work.
"""
# Store the new app
self.app = app
# And create a new app server. This is necessary, because the mounted
# sub-apps may have changed. This ensures they're up to date.
if app_server is None:
self._create_and_store_app_server()
else:
self.app_server = app_server
self.app_server.debug_mode = self.debug_mode
self.app_server.running_in_window = self.run_in_window
self.app_server.internal_on_app_start = (
lambda: self.on_server_is_ready_or_failed.set_result(None)
)
if self.base_url is None:
self.app_server.base_url = None
else:
self.app_server.base_url = utils.normalize_url(self.base_url)
assert self.app_server is not None
rio.cli._logger.debug("Replacing the app in the server")
self.app_server.app = app
assert self.app_server.app is self.app
# There is no need to inject the new app or server anywhere. Since
# uvicorn was fed a shim function instead of the app directly, any
# requests will automatically be redirected to the new server instance.

View File

@@ -1,13 +1,13 @@
import threading
import time
from typing import * # type: ignore
import typing as t
from . import run_models
try:
import webview # type: ignore
except ImportError:
if TYPE_CHECKING:
if t.TYPE_CHECKING:
import webview # type: ignore
else:
webview = None
@@ -17,7 +17,7 @@ class WebViewWorker:
def __init__(
self,
*,
push_event: Callable[[run_models.Event], None],
push_event: t.Callable[[run_models.Event], None],
debug_mode: bool,
url: str,
) -> None:
@@ -26,7 +26,7 @@ class WebViewWorker:
self.url = url
# If running, this is the webview window
self.window: Optional[webview.Window] = None
self.window: webview.Window | None = None
def run(self) -> None:
"""

View File

@@ -2,9 +2,9 @@ from __future__ import annotations
import colorsys
import math
from typing import * # type: ignore
import typing as t
from typing_extensions import TypeAlias
import typing_extensions as te
from uniserde import Jsonable
import rio
@@ -17,7 +17,7 @@ __all__ = [
]
@final
@t.final
class Color(SelfSerializing):
"""
A color, optionally with an opacity.
@@ -478,7 +478,7 @@ class Color(SelfSerializing):
opacity=self.opacity if opacity is None else opacity,
)
def _map_rgb(self, func: Callable[[float], float]) -> "Color":
def _map_rgb(self, func: t.Callable[[float], float]) -> "Color":
"""
Apply a function to each of the RGB values of this color, and return a
new `Color` instance with the result. The opacity value is copied
@@ -631,28 +631,28 @@ class Color(SelfSerializing):
return hash(self.rgba)
# Greys
BLACK: ClassVar["Color"]
GREY: ClassVar["Color"]
WHITE: ClassVar["Color"]
BLACK: t.ClassVar["Color"]
GREY: t.ClassVar["Color"]
WHITE: t.ClassVar["Color"]
# Pure colors
RED: ClassVar["Color"]
GREEN: ClassVar["Color"]
BLUE: ClassVar["Color"]
RED: t.ClassVar["Color"]
GREEN: t.ClassVar["Color"]
BLUE: t.ClassVar["Color"]
# CMY
CYAN: ClassVar["Color"]
MAGENTA: ClassVar["Color"]
YELLOW: ClassVar["Color"]
CYAN: t.ClassVar["Color"]
MAGENTA: t.ClassVar["Color"]
YELLOW: t.ClassVar["Color"]
# Others
PINK: ClassVar["Color"]
PURPLE: ClassVar["Color"]
ORANGE: ClassVar["Color"]
BROWN: ClassVar["Color"]
PINK: t.ClassVar["Color"]
PURPLE: t.ClassVar["Color"]
ORANGE: t.ClassVar["Color"]
BROWN: t.ClassVar["Color"]
# Special
TRANSPARENT: ClassVar["Color"]
TRANSPARENT: t.ClassVar["Color"]
Color.BLACK = Color.from_rgb(0.0, 0.0, 0.0)
@@ -675,9 +675,9 @@ Color.TRANSPARENT = Color.from_rgb(0.0, 0.0, 0.0, 0.0)
# Like color, but also allows referencing theme colors
ColorSet: TypeAlias = (
ColorSet: te.TypeAlias = (
Color
| Literal[
| t.Literal[
"background",
"neutral",
"hud",
@@ -693,4 +693,4 @@ ColorSet: TypeAlias = (
# Cache so the session can quickly determine whether a type annotation is
# `ColorSet`
_color_set_args = set(get_args(ColorSet))
_color_set_args = set(t.get_args(ColorSet))

View File

@@ -2,14 +2,14 @@ from __future__ import annotations
import asyncio
import sys
import typing as t
import warnings
import weakref
from collections import defaultdict
from dataclasses import field
from typing import *
import introspection
from typing_extensions import dataclass_transform
import typing_extensions as te
import rio
@@ -22,12 +22,12 @@ from .warnings import RioPotentialMistakeWarning
__all__ = ["ComponentMeta"]
C = TypeVar("C", bound="rio.Component")
C = t.TypeVar("C", bound="rio.Component")
# For some reason vscode doesn't understand that this class is a
# `@dataclass_transform`, so we'll annotate it again...
@dataclass_transform(
# `@te.dataclass_transform`, so we'll annotate it again...
@te.dataclass_transform(
eq_default=False,
field_specifiers=(internal_field, field),
)
@@ -41,7 +41,7 @@ class ComponentMeta(RioDataclassMeta):
# The assigned value is needed so that the `Component` class itself has a
# valid value. All subclasses override this value in `__init_subclass__`.
_rio_event_handlers_: defaultdict[
event.EventTag, list[tuple[Callable, Any]]
event.EventTag, list[tuple[t.Callable, t.Any]]
]
# Whether this component class is built into Rio, rather than user defined,
@@ -93,7 +93,7 @@ class ComponentMeta(RioDataclassMeta):
continue
try:
events = member._rio_events_
events = member._rio_events_ # type: ignore
except AttributeError:
continue
@@ -238,7 +238,7 @@ class ComponentMeta(RioDataclassMeta):
async def _periodic_event_worker(
weak_component: weakref.ReferenceType[rio.Component],
handler: Callable,
handler: t.Callable,
period: float,
) -> None:
# Get a handle on the session
@@ -264,7 +264,7 @@ async def _periodic_event_worker(
async def call_component_handler_once(
weak_component: weakref.ReferenceType[rio.Component],
handler: Callable,
handler: t.Callable,
) -> bool:
# Does the component still exist?
component = weak_component()

View File

@@ -39,6 +39,7 @@ from .number_input import *
from .overlay import *
from .page_view import *
from .plot import *
from .pointer_event_listener import *
from .popup import *
from .progress_bar import *
from .progress_circle import *

View File

@@ -1,7 +1,7 @@
from __future__ import annotations
import typing as t
from dataclasses import KW_ONLY
from typing import * # type: ignore
import rio
@@ -116,7 +116,7 @@ class Sidebar(component.Component):
class AppRoot(component.Component):
_: KW_ONLY
fallback_build: Callable[[], rio.Component] | None = None
fallback_build: t.Callable[[], rio.Component] | None = None
_sidebar_is_open: bool = False

View File

@@ -2,8 +2,8 @@ from __future__ import annotations
import dataclasses
import enum
import typing as t
from dataclasses import KW_ONLY, dataclass, is_dataclass
from typing import * # type: ignore
import rio
@@ -20,7 +20,7 @@ def prettify_name(name: str) -> str:
@dataclass
class AutoFormChangeEvent:
field_name: str
value: Any
value: t.Any
class AutoForm(component.Component):
@@ -32,7 +32,7 @@ class AutoForm(component.Component):
`public`: False
"""
value: Any
value: t.Any
_: KW_ONLY
on_change: rio.EventHandler[[AutoFormChangeEvent]] = None
@@ -43,7 +43,7 @@ class AutoForm(component.Component):
f"The value to `AutoForm` must be a dataclass, not `{type(self.value)}`"
)
async def _update_value(self, field_name: str, value: Any) -> None:
async def _update_value(self, field_name: str, value: t.Any) -> None:
# Update the value
setattr(self, field_name, value)
@@ -62,8 +62,8 @@ class AutoForm(component.Component):
field_type: type,
) -> rio.Component:
# Get sensible type information
origin = get_origin(field_type)
field_args = get_args(field_type)
origin = t.get_origin(field_type)
field_args = t.get_args(field_type)
field_type = field_type if origin is None else origin
del origin
@@ -100,11 +100,11 @@ class AutoForm(component.Component):
)
# `Literal` or `Enum` -> `Dropdown`
if field_type is Literal or issubclass(field_type, enum.Enum):
if field_type is Literal:
if field_type is t.Literal or issubclass(field_type, enum.Enum):
if field_type is t.Literal:
mapping = {str(a): a for a in field_args}
else:
field_type = cast(Type[enum.Enum], field_type)
field_type = t.cast(t.Type[enum.Enum], field_type)
mapping = {prettify_name(f.name): f.value for f in field_type}
return rio.Dropdown(

View File

@@ -1,7 +1,7 @@
from __future__ import annotations
import typing as t
from dataclasses import KW_ONLY
from typing import * # type: ignore
import rio
@@ -19,7 +19,7 @@ ICONS_AND_COLORS: dict[str, tuple[str, rio.ColorSet]] = {
}
@final
@t.final
class Banner(component.Component):
r"""
Displays a short message to the user.
@@ -84,7 +84,7 @@ class Banner(component.Component):
"""
text: str | None
style: Literal["info", "success", "warning", "danger"]
style: t.Literal["info", "success", "warning", "danger"]
_: KW_ONLY
markdown: bool = False

View File

@@ -1,7 +1,7 @@
from __future__ import annotations
import typing as t
from dataclasses import KW_ONLY
from typing import * # type: ignore
from uniserde import JsonDoc
@@ -19,7 +19,7 @@ CHILD_MARGIN_X = 1.0
CHILD_MARGIN_Y = 0.3
@final
@t.final
class Button(Component):
"""
A clickable button.
@@ -112,10 +112,10 @@ class Button(Component):
content: str | rio.Component = ""
_: KW_ONLY
icon: str | None = None
shape: Literal["pill", "rounded", "rectangle"] = "pill"
style: Literal["major", "minor", "colored-text", "plain-text", "plain"] = (
"major"
)
shape: t.Literal["pill", "rounded", "rectangle"] = "pill"
style: t.Literal[
"major", "minor", "colored-text", "plain-text", "plain"
] = "major"
color: rio.ColorSet = "keep"
is_sensitive: bool = True
is_loading: bool = False
@@ -207,10 +207,10 @@ class _ButtonInternal(FundamentalComponent):
_: KW_ONLY
on_press: rio.EventHandler[[]]
content: rio.Component
shape: Literal["pill", "rounded", "rectangle", "circle"]
style: Literal["major", "minor", "colored-text", "plain-text", "plain"] = (
"major"
)
shape: t.Literal["pill", "rounded", "rectangle", "circle"]
style: t.Literal[
"major", "minor", "colored-text", "plain-text", "plain"
] = "major"
color: rio.ColorSet
is_sensitive: bool
is_loading: bool
@@ -230,7 +230,7 @@ class _ButtonInternal(FundamentalComponent):
return {}
async def _on_message_(self, msg: Any) -> None:
async def _on_message_(self, msg: t.Any) -> None:
# Parse the message
assert isinstance(msg, dict), msg
assert msg["type"] == "press", msg

View File

@@ -1,8 +1,8 @@
from __future__ import annotations
import typing as t
from dataclasses import KW_ONLY, dataclass
from datetime import date
from typing import * # type: ignore
from uniserde import JsonDoc
@@ -16,7 +16,7 @@ __all__ = [
]
@final
@t.final
@rio.docs.mark_constructor_as_private
@dataclass
class DateChangeEvent:
@@ -122,7 +122,7 @@ class Calendar(FundamentalComponent):
"firstDayOfWeek": self.session._first_day_of_week,
}
async def _on_message_(self, msg: Any) -> None:
async def _on_message_(self, msg: t.Any) -> None:
# Parse the message
assert isinstance(msg, dict), msg

View File

@@ -1,7 +1,7 @@
from __future__ import annotations
import typing as t
from dataclasses import KW_ONLY
from typing import * # type: ignore
from uniserde import JsonDoc
@@ -14,7 +14,7 @@ __all__ = [
]
@final
@t.final
class Card(FundamentalComponent):
"""
A container that visually encompasses its content.
@@ -111,7 +111,7 @@ class Card(FundamentalComponent):
colorize_on_hover: bool | None = None
color: rio.ColorSet = "neutral"
async def _on_message_(self, msg: Any) -> None:
async def _on_message_(self, msg: t.Any) -> None:
# Trigger the press event
await self.call_event_handler(self.on_press)

View File

@@ -1,7 +1,7 @@
from __future__ import annotations
import typing as t
from dataclasses import KW_ONLY, dataclass
from typing import * # type: ignore
from uniserde import JsonDoc
@@ -15,13 +15,25 @@ __all__ = [
]
@final
@t.final
@dataclass
class CheckboxChangeEvent:
"""
Holds information regarding a checkbox change event.
This is a simple dataclass that stores useful information for when the user
switches a `CheckBox` on or off. You'll typically receive this as argument
in `on_change` events.
## Attributes
`is_on`: Whether the checkbox is now ticked.
"""
is_on: bool
@final
@t.final
class Checkbox(FundamentalComponent):
"""
An input for `True` / `False` values.

View File

@@ -1,6 +1,6 @@
from __future__ import annotations
from typing import Any, Sequence
import typing as t
import rio
@@ -36,9 +36,9 @@ class ClassContainer(FundamentalComponent):
"""
content: rio.Component | None
classes: Sequence[str]
classes: t.Sequence[str]
def _get_debug_details_(self) -> dict[str, Any]:
def _get_debug_details_(self) -> dict[str, t.Any]:
result = super()._get_debug_details_()
result.pop("classes")
return result

Some files were not shown because too many files have changed in this diff Show More