mirror of
https://github.com/rio-labs/rio.git
synced 2025-12-31 02:09:48 -06:00
dialog improvements
This commit is contained in:
@@ -390,7 +390,6 @@ export function recursivelyDeleteComponent(component: ComponentBase): void {
|
||||
|
||||
// Inform Python about the destruction of the dialog
|
||||
callRemoteMethodDiscardResponse('dialogClosed', {
|
||||
owningComponentId: this.state.owning_component_id,
|
||||
dialogRootComponentId: dialog_container.id,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
ClickHandlerArguments,
|
||||
ClickHandler,
|
||||
} from '../eventHandling';
|
||||
import { ComponentId, RioScrollBehavior } from '../dataModels';
|
||||
import { ComponentId } from '../dataModels';
|
||||
import { insertWrapperElement, replaceElement } from '../utils';
|
||||
import { devToolsConnector } from '../app';
|
||||
import { DialogContainerComponent } from './dialog_container';
|
||||
@@ -69,7 +69,7 @@ export abstract class ComponentBase {
|
||||
|
||||
// Any dialogs attached to this component. When this component disappears,
|
||||
// the dialogs go with it.
|
||||
ownedDialogs: DialogContainerComponent[] = [];
|
||||
public ownedDialogs: DialogContainerComponent[] = [];
|
||||
|
||||
constructor(id: ComponentId, state: Required<ComponentState>) {
|
||||
this.id = id;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { recursivelyDeleteComponent } from '../componentManagement';
|
||||
import { ComponentId } from '../dataModels';
|
||||
import { markEventAsHandled } from '../eventHandling';
|
||||
import { callRemoteMethodDiscardResponse } from '../rpc';
|
||||
import { commitCss } from '../utils';
|
||||
import { ComponentBase, ComponentState } from './componentBase';
|
||||
@@ -8,8 +9,8 @@ export type DialogContainerState = ComponentState & {
|
||||
_type_: 'DialogContainer-builtin';
|
||||
content?: ComponentId;
|
||||
owning_component_id?: ComponentId;
|
||||
modal?: boolean;
|
||||
user_closable?: boolean;
|
||||
is_modal?: boolean;
|
||||
is_user_closable?: boolean;
|
||||
};
|
||||
|
||||
export class DialogContainerComponent extends ComponentBase {
|
||||
@@ -32,8 +33,10 @@ export class DialogContainerComponent extends ComponentBase {
|
||||
|
||||
// Listen for outside clicks
|
||||
element.addEventListener('click', (event) => {
|
||||
markEventAsHandled(event);
|
||||
|
||||
// Is the dialog user-closable?
|
||||
if (!this.state.user_closable) {
|
||||
if (!this.state.is_user_closable) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -83,7 +86,7 @@ export class DialogContainerComponent extends ComponentBase {
|
||||
this.replaceOnlyChild(latentComponents, deltaState.content);
|
||||
|
||||
// Modal
|
||||
if (deltaState.modal) {
|
||||
if (deltaState.is_modal) {
|
||||
this.element.style.pointerEvents = 'auto';
|
||||
this.element.style.removeProperty('background-color');
|
||||
} else {
|
||||
|
||||
@@ -33,9 +33,7 @@ export class ImageComponent extends ComponentBase {
|
||||
this.imageElement = document.createElement('img');
|
||||
element.appendChild(this.imageElement);
|
||||
|
||||
this.imageElement.onload = () => {
|
||||
this.imageElement.classList.remove('rio-content-loading');
|
||||
};
|
||||
this.imageElement.onload = this._onLoad.bind(this);
|
||||
this.imageElement.onerror = this._onError.bind(this);
|
||||
|
||||
return element;
|
||||
@@ -80,6 +78,14 @@ export class ImageComponent extends ComponentBase {
|
||||
}
|
||||
}
|
||||
|
||||
private _onLoad(): void {
|
||||
this.imageElement.classList.remove('rio-content-loading');
|
||||
|
||||
// Browsers are dumb and render content outside of the SVG viewbox if
|
||||
// the <img> element is too large. So we can't set `width/height: 100%`
|
||||
// as we usually would.
|
||||
}
|
||||
|
||||
private _onError(event: string | Event): void {
|
||||
this.imageElement.classList.remove('rio-content-loading');
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
UnittestClientLayoutInfo,
|
||||
UnittestComponentLayout,
|
||||
} from './dataModels';
|
||||
import { DialogContainerComponent } from './components/dialog_container';
|
||||
|
||||
export async function registerFont(
|
||||
name: string,
|
||||
|
||||
@@ -102,6 +102,7 @@ dev-dependencies = [
|
||||
"polars>=0.20",
|
||||
"pre-commit~=3.1",
|
||||
"pytest~=8.2.1",
|
||||
"pywebview",
|
||||
"requests~=2.31",
|
||||
"ruff>=0.4.7",
|
||||
"selenium>=4.22",
|
||||
|
||||
51
rio/app.py
51
rio/app.py
@@ -492,7 +492,12 @@ class App:
|
||||
|
||||
def run_in_window(
|
||||
self,
|
||||
*,
|
||||
quiet: bool = True,
|
||||
maximized: bool = False,
|
||||
fullscreen: bool = False,
|
||||
width: float | None = None,
|
||||
height: float | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
Runs the app in a local window.
|
||||
@@ -567,12 +572,54 @@ class App:
|
||||
# Wait for the server to start
|
||||
app_ready_event.wait()
|
||||
|
||||
# Problem: width and height are given in rem, but we need them in
|
||||
# pixels. We'll use pywebview's execute_js to find out as soon as the
|
||||
# window has been created, and then update the window size accordingly.
|
||||
def update_window_size():
|
||||
if width is None and height is None:
|
||||
return
|
||||
|
||||
pixels_per_rem = window.evaluate_js("""
|
||||
let measure = document.createElement('div');
|
||||
measure.style.height = '1rem';
|
||||
|
||||
let pixels_per_rem = measure.getBoundingClientRect().height *
|
||||
window.devicePixelRatio;
|
||||
|
||||
measure.remove();
|
||||
|
||||
pixels_per_rem
|
||||
""")
|
||||
|
||||
if width is None:
|
||||
width_in_pixels = window.width
|
||||
else:
|
||||
width_in_pixels = round(width * pixels_per_rem)
|
||||
|
||||
if height is None:
|
||||
height_in_pixels = window.height
|
||||
else:
|
||||
height_in_pixels = round(height * pixels_per_rem)
|
||||
|
||||
window.set_window_size(width_in_pixels, height_in_pixels)
|
||||
|
||||
# Start the webview
|
||||
try:
|
||||
webview.create_window(self.name, url)
|
||||
webview.start(debug=os.environ.get("RIO_WEBVIEW_DEBUG") == "1")
|
||||
window = webview.create_window(
|
||||
self.name,
|
||||
url,
|
||||
maximized=maximized,
|
||||
fullscreen=fullscreen,
|
||||
)
|
||||
webview.start(
|
||||
update_window_size,
|
||||
debug=os.environ.get("RIO_WEBVIEW_DEBUG") == "1",
|
||||
)
|
||||
|
||||
finally:
|
||||
server = cast(
|
||||
uvicorn.Server, server
|
||||
) # Prevents "unreachable code" warning
|
||||
assert isinstance(server, uvicorn.Server)
|
||||
|
||||
server.should_exit = True
|
||||
|
||||
@@ -474,12 +474,22 @@ class Component(abc.ABC, metaclass=ComponentMeta):
|
||||
and builder._is_in_component_tree(cache)
|
||||
)
|
||||
|
||||
# Special case: DialogContainers are always considered to be part of the
|
||||
# component tree
|
||||
# Special case: DialogContainers are considered to be part of the
|
||||
# component tree as long as their owning component is
|
||||
if not result and isinstance(
|
||||
self, rio.components.dialog_container.DialogContainer
|
||||
):
|
||||
result = True
|
||||
try:
|
||||
owning_component = self.session._weak_components_by_id[
|
||||
self.owning_component_id
|
||||
]
|
||||
except KeyError:
|
||||
result = False
|
||||
else:
|
||||
result = (
|
||||
owning_component._is_in_component_tree(cache)
|
||||
and self._id in owning_component._owned_dialogs_
|
||||
)
|
||||
|
||||
# Cache the result and return
|
||||
cache[self] = result
|
||||
@@ -702,8 +712,8 @@ class Component(abc.ABC, metaclass=ComponentMeta):
|
||||
dialog_container = dialog_container.DialogContainer(
|
||||
build_content=build,
|
||||
owning_component_id=self._id,
|
||||
modal=modal,
|
||||
user_closeable=user_closeable,
|
||||
is_modal=modal,
|
||||
is_user_closeable=user_closeable,
|
||||
on_close=on_close,
|
||||
)
|
||||
|
||||
|
||||
@@ -16,8 +16,8 @@ __all__ = [
|
||||
class DialogContainer(Component):
|
||||
build_content: Callable[[], Component]
|
||||
owning_component_id: int
|
||||
modal: bool
|
||||
user_closeable: bool
|
||||
is_modal: bool
|
||||
is_user_closeable: bool
|
||||
on_close: rio.EventHandler[[]]
|
||||
|
||||
def build(self) -> Component:
|
||||
@@ -30,6 +30,6 @@ class DialogContainer(Component):
|
||||
def serialize(self) -> dict[str, Any]:
|
||||
return {
|
||||
"owning_component_id": self.owning_component_id,
|
||||
"modal": self.modal,
|
||||
"user_closable": self.user_closeable,
|
||||
"is_modal": self.is_modal,
|
||||
"is_user_closable": self.is_user_closeable,
|
||||
}
|
||||
|
||||
@@ -15,11 +15,7 @@ class Dialog:
|
||||
"Dialogs cannot be instantiated directly. To create a dialog, call `self.show_custom_dialog` inside of a component's event handler."
|
||||
)
|
||||
|
||||
async def close(self) -> None:
|
||||
"""
|
||||
Removes the dialog from the screen. Has no effect if the dialog has
|
||||
already been previously closed.
|
||||
"""
|
||||
def _cleanup(self) -> None:
|
||||
# Try to remove the dialog from its owning component. This can fail if
|
||||
# the dialog has already been removed.
|
||||
try:
|
||||
@@ -27,6 +23,13 @@ class Dialog:
|
||||
except KeyError:
|
||||
return
|
||||
|
||||
async def close(self) -> None:
|
||||
"""
|
||||
Removes the dialog from the screen. Has no effect if the dialog has
|
||||
already been previously closed.
|
||||
"""
|
||||
self._cleanup()
|
||||
|
||||
# The dialog was just discarded on the Python side. Tell the client to
|
||||
# also remove it.
|
||||
await self._root_component.session._remove_dialog(
|
||||
|
||||
105
rio/session.py
105
rio/session.py
@@ -41,7 +41,7 @@ from . import (
|
||||
user_settings_module,
|
||||
utils,
|
||||
)
|
||||
from .components import fundamental_component, root_components
|
||||
from .components import dialog_container, fundamental_component, root_components
|
||||
from .data_models import BuildData
|
||||
from .state_properties import AttributeBinding
|
||||
from .transports import AbstractTransport, TransportInterrupted
|
||||
@@ -160,6 +160,9 @@ class Session(unicall.Unicall):
|
||||
self.window_width = window_width
|
||||
self.window_height = window_height
|
||||
|
||||
self._is_maximized = False
|
||||
self._is_fullscreen = False
|
||||
|
||||
self._base_url = base_url
|
||||
self.theme = theme_
|
||||
|
||||
@@ -485,6 +488,53 @@ class Session(unicall.Unicall):
|
||||
# useful in a window anymore.
|
||||
return self.http_headers.get("user-agent", "")
|
||||
|
||||
# @property
|
||||
# def is_maximized(self) -> bool:
|
||||
# return self._is_maximized
|
||||
|
||||
# @is_maximized.setter
|
||||
# def is_maximized(self, is_maximized: bool) -> None:
|
||||
# self._is_maximized = is_maximized
|
||||
# self.create_task(self._set_maximized(is_maximized))
|
||||
|
||||
async def _set_maximized(self, is_maximized: bool) -> None:
|
||||
if self.running_in_window:
|
||||
window = await self._get_webview_window()
|
||||
|
||||
if is_maximized:
|
||||
window.maximize()
|
||||
else:
|
||||
raise NotImplementedError # FIXME
|
||||
else:
|
||||
if is_maximized:
|
||||
await self._evaluate_javascript_and_get_result("""
|
||||
window.moveTo(0, 0);
|
||||
window.resizeTo(screen.availWidth, screen.availHeight);
|
||||
""")
|
||||
else:
|
||||
raise NotImplementedError # FIXME
|
||||
|
||||
# @property
|
||||
# def is_fullscreen(self) -> bool:
|
||||
# return self._is_fullscreen
|
||||
|
||||
# @is_fullscreen.setter
|
||||
# def is_fullscreen(self, fullscreen: bool):
|
||||
# self._is_fullscreen = fullscreen
|
||||
# self.create_task(self._set_fullscreen(fullscreen))
|
||||
|
||||
async def _set_fullscreen(self, fullscreen: bool) -> None:
|
||||
if self.running_in_window:
|
||||
window = await self._get_webview_window()
|
||||
window.toggle_fullscreen()
|
||||
else:
|
||||
if fullscreen:
|
||||
js_code = "await document.documentElement.requestFullscreen();"
|
||||
else:
|
||||
js_code = "document.exitFullscreen();"
|
||||
|
||||
await self._evaluate_javascript_and_get_result(js_code)
|
||||
|
||||
@property
|
||||
def _is_connected(self) -> bool:
|
||||
"""
|
||||
@@ -2446,28 +2496,39 @@ a.remove();
|
||||
await component._on_message(payload)
|
||||
|
||||
@unicall.local(name="dialogClosed")
|
||||
async def _dialog_closed(
|
||||
self,
|
||||
owning_component_id: int,
|
||||
dialog_root_component_id: int,
|
||||
) -> None:
|
||||
# Fetch the owning component
|
||||
dialog = self._weak_components_by_id[owning_component_id]
|
||||
|
||||
# Don't die to network lag
|
||||
if dialog is None:
|
||||
return
|
||||
|
||||
async def _dialog_closed(self, dialog_root_component_id: int) -> None:
|
||||
# Fetch and remove the dialog itself, while still not succumbing to
|
||||
# network lag
|
||||
try:
|
||||
dialog = dialog._owned_dialogs_.pop(dialog_root_component_id)
|
||||
dialog_cont = self._weak_components_by_id[dialog_root_component_id]
|
||||
except KeyError:
|
||||
return
|
||||
|
||||
assert isinstance(
|
||||
dialog_cont, dialog_container.DialogContainer
|
||||
), dialog_cont
|
||||
|
||||
# Fetch the owning component
|
||||
try:
|
||||
dialog_owner = self._weak_components_by_id[
|
||||
dialog_cont.owning_component_id
|
||||
]
|
||||
except KeyError:
|
||||
# Don't die to network lag
|
||||
return
|
||||
|
||||
# Fetch the Dialog object and mark it as closed
|
||||
try:
|
||||
dialog = dialog_owner._owned_dialogs_[dialog_root_component_id]
|
||||
except KeyError:
|
||||
# Don't die to network lag
|
||||
return
|
||||
|
||||
dialog._cleanup()
|
||||
|
||||
# Trigger the dialog's close event
|
||||
await self._call_event_handler(
|
||||
dialog._root_component.on_close,
|
||||
dialog_cont.on_close,
|
||||
refresh=True,
|
||||
)
|
||||
|
||||
@@ -2537,6 +2598,20 @@ a.remove();
|
||||
name="`on_on_window_size_change` event handler",
|
||||
)
|
||||
|
||||
@unicall.local(name="onFullscreenChange")
|
||||
async def _on_fullscreen_change(self, fullscreen: bool) -> None:
|
||||
"""
|
||||
Called by the client when the window is (un-)fullscreened.
|
||||
"""
|
||||
self._is_fullscreen = fullscreen
|
||||
|
||||
@unicall.local(name="onMaximizedChange")
|
||||
async def _on_maximized_change(self, maximized: bool) -> None:
|
||||
"""
|
||||
Called by the client when the window is (un-)maximized.
|
||||
"""
|
||||
self._is_maximized = maximized
|
||||
|
||||
@unicall.remote(
|
||||
name="setClipboard",
|
||||
parameter_format="dict",
|
||||
|
||||
Reference in New Issue
Block a user