mirror of
https://github.com/rio-labs/rio.git
synced 2026-02-09 07:09:00 -06:00
Merge branch 'main' into patch-1
This commit is contained in:
116
changelog.md
116
changelog.md
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -11,7 +11,7 @@ export type FlowState = ComponentState & {
|
||||
};
|
||||
|
||||
export class FlowComponent extends ComponentBase {
|
||||
state: Required<FlowState>;
|
||||
declare state: Required<FlowState>;
|
||||
|
||||
private innerElement: HTMLElement;
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ export type FundamentalRootComponentState = ComponentState & {
|
||||
};
|
||||
|
||||
export class FundamentalRootComponent extends ComponentBase {
|
||||
state: Required<FundamentalRootComponentState>;
|
||||
declare state: Required<FundamentalRootComponentState>;
|
||||
|
||||
public overlaysContainer: HTMLElement;
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -6,7 +6,7 @@ export type HtmlState = ComponentState & {
|
||||
};
|
||||
|
||||
export class HtmlComponent extends ComponentBase {
|
||||
state: Required<HtmlState>;
|
||||
declare state: Required<HtmlState>;
|
||||
|
||||
private isInitialized = false;
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ export type IconState = ComponentState & {
|
||||
};
|
||||
|
||||
export class IconComponent extends ComponentBase {
|
||||
state: Required<IconState>;
|
||||
declare state: Required<IconState>;
|
||||
|
||||
private svgElement: SVGSVGElement;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ export type NodeInputState = ComponentState & {
|
||||
};
|
||||
|
||||
export class NodeInputComponent extends ComponentBase {
|
||||
state: Required<NodeInputState>;
|
||||
declare state: Required<NodeInputState>;
|
||||
|
||||
textElement: HTMLElement;
|
||||
circleElement: HTMLElement;
|
||||
|
||||
@@ -10,7 +10,7 @@ export type NodeOutputState = ComponentState & {
|
||||
};
|
||||
|
||||
export class NodeOutputComponent extends ComponentBase {
|
||||
state: Required<NodeOutputState>;
|
||||
declare state: Required<NodeOutputState>;
|
||||
|
||||
textElement: HTMLElement;
|
||||
circleElement: HTMLElement;
|
||||
|
||||
@@ -8,7 +8,7 @@ export type OverlayState = ComponentState & {
|
||||
};
|
||||
|
||||
export class OverlayComponent extends ComponentBase {
|
||||
state: Required<OverlayState>;
|
||||
declare state: Required<OverlayState>;
|
||||
|
||||
private overlayElement: HTMLElement;
|
||||
|
||||
|
||||
@@ -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, ...)
|
||||
|
||||
215
frontend/code/components/pointerEventListener.ts
Normal file
215
frontend/code/components/pointerEventListener.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -10,7 +10,7 @@ export type ProgressBarState = ComponentState & {
|
||||
};
|
||||
|
||||
export class ProgressBarComponent extends ComponentBase {
|
||||
state: Required<ProgressBarState>;
|
||||
declare state: Required<ProgressBarState>;
|
||||
|
||||
fillElement: HTMLElement;
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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");
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -13,7 +13,7 @@ export type ScrollTargetState = ComponentState & {
|
||||
};
|
||||
|
||||
export class ScrollTargetComponent extends ComponentBase {
|
||||
state: Required<ScrollTargetState>;
|
||||
declare state: Required<ScrollTargetState>;
|
||||
|
||||
childContainerElement: HTMLElement;
|
||||
buttonContainerElement: HTMLElement;
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ export type TextState = ComponentState & {
|
||||
};
|
||||
|
||||
export class TextComponent extends ComponentBase {
|
||||
state: Required<TextState>;
|
||||
declare state: Required<TextState>;
|
||||
|
||||
private inner: HTMLElement;
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -60,6 +60,7 @@ export type TextStyle = {
|
||||
italic: boolean;
|
||||
fontWeight: "normal" | "bold";
|
||||
underlined: boolean;
|
||||
strikethrough: boolean;
|
||||
allCaps: boolean;
|
||||
};
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
283
rio/app.py
283
rio/app.py
@@ -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()
|
||||
|
||||
@@ -11,7 +11,6 @@ import warnings
|
||||
import weakref
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
from typing import *
|
||||
|
||||
import langcodes
|
||||
import pytz
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import *
|
||||
|
||||
import rio
|
||||
|
||||
from .. import assets, utils
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import * # type: ignore
|
||||
|
||||
import keyring
|
||||
import platformdirs
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 -
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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:
|
||||
"""
|
||||
|
||||
42
rio/color.py
42
rio/color.py
@@ -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))
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 *
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user