mirror of
https://github.com/rio-labs/rio.git
synced 2026-02-10 23:59:10 -06:00
implement resize observer for component size changes
This commit is contained in:
@@ -13,8 +13,13 @@ import {
|
||||
ClickHandler,
|
||||
} from "../eventHandling";
|
||||
import { ComponentId } from "../dataModels";
|
||||
import { insertWrapperElement, replaceElement } from "../utils";
|
||||
import { devToolsConnector } from "../app";
|
||||
import {
|
||||
getAllocatedHeightInPx,
|
||||
getAllocatedWidthInPx,
|
||||
insertWrapperElement,
|
||||
replaceElement,
|
||||
} from "../utils";
|
||||
import { devToolsConnector, pixelsPerRem } from "../app";
|
||||
|
||||
export type Key = string | number;
|
||||
|
||||
@@ -46,6 +51,8 @@ export type ComponentState = {
|
||||
// Debugging information: The dev tools may not display components to the
|
||||
// developer if they're considered internal
|
||||
_rio_internal_: boolean;
|
||||
// Whether this component requested resize events
|
||||
_on_resize_: boolean;
|
||||
};
|
||||
|
||||
export type DeltaState<S extends ComponentState> = Omit<Partial<S>, "_type_">;
|
||||
@@ -76,6 +83,7 @@ export abstract class ComponentBase<S extends ComponentState = ComponentState> {
|
||||
private outerScrollElement: HTMLElement | null = null;
|
||||
private centerScrollElement: HTMLElement | null = null;
|
||||
private innerScrollElement: HTMLElement | null = null;
|
||||
private sizeObserver: ResizeObserver | null = null;
|
||||
|
||||
constructor(
|
||||
id: ComponentId,
|
||||
@@ -151,10 +159,29 @@ export abstract class ComponentBase<S extends ComponentState = ComponentState> {
|
||||
this.element.role = deltaState.accessibility_role;
|
||||
}
|
||||
}
|
||||
|
||||
if (deltaState._on_resize_ !== undefined) {
|
||||
if (deltaState._on_resize_ && this.sizeObserver === null) {
|
||||
this.sizeObserver = new ResizeObserver(
|
||||
this._onSizeChange.bind(this)
|
||||
);
|
||||
this.sizeObserver.observe(this.element);
|
||||
}
|
||||
if (!deltaState._on_resize_ && this.sizeObserver !== null) {
|
||||
this.sizeObserver.disconnect();
|
||||
this.sizeObserver = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onChildGrowChanged(): void {}
|
||||
|
||||
private _onSizeChange(): void {
|
||||
let width = getAllocatedWidthInPx(this.element);
|
||||
let height = getAllocatedHeightInPx(this.element);
|
||||
this.triggerResizeEvent(width / pixelsPerRem, height / pixelsPerRem);
|
||||
}
|
||||
|
||||
private _updateMaxSize(maxSize: [number | null, number | null]): void {
|
||||
let transform: string[] = [];
|
||||
|
||||
@@ -558,6 +585,9 @@ export abstract class ComponentBase<S extends ComponentState = ComponentState> {
|
||||
for (let handler of this._eventHandlers) {
|
||||
handler.disconnect();
|
||||
}
|
||||
if (this.sizeObserver !== null) {
|
||||
this.sizeObserver.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
/// Send a message to the python instance corresponding to this component. The
|
||||
@@ -570,6 +600,14 @@ export abstract class ComponentBase<S extends ComponentState = ComponentState> {
|
||||
});
|
||||
}
|
||||
|
||||
triggerResizeEvent(width: number, height: number): void {
|
||||
callRemoteMethodDiscardResponse("onComponentSizeChange", {
|
||||
component_id: this.id,
|
||||
new_width: width,
|
||||
new_height: height,
|
||||
});
|
||||
}
|
||||
|
||||
_setStateDontNotifyBackend(deltaState: DeltaState<S>): void {
|
||||
// Trigger an update
|
||||
this.updateElement(
|
||||
|
||||
@@ -39,6 +39,7 @@ class EventTag(enum.Enum):
|
||||
ON_MOUNT = enum.auto()
|
||||
ON_PAGE_CHANGE = enum.auto()
|
||||
ON_POPULATE = enum.auto()
|
||||
ON_RESIZE = enum.auto()
|
||||
ON_UNMOUNT = enum.auto()
|
||||
ON_WINDOW_SIZE_CHANGE = enum.auto()
|
||||
PERIODIC = enum.auto()
|
||||
@@ -386,3 +387,10 @@ def periodic(
|
||||
return handler
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def on_resize(
|
||||
handler: t.Callable[[t.Any, float, float], None],
|
||||
) -> t.Callable[[t.Any, float, float], None]:
|
||||
_tag_as_event_handler(handler, EventTag.ON_RESIZE, None)
|
||||
return handler
|
||||
|
||||
@@ -182,6 +182,9 @@ def serialize_and_host_component(
|
||||
result["_type_"] = "HighLevelComponent-builtin"
|
||||
result["_child_"] = component._build_data_.build_result._id_ # type: ignore
|
||||
|
||||
if rio.event.EventTag.ON_RESIZE in component._rio_event_handlers_:
|
||||
result["_on_resize_"] = True
|
||||
|
||||
return result
|
||||
|
||||
|
||||
|
||||
@@ -3905,6 +3905,25 @@ a.remove();
|
||||
name="`on_on_window_size_change` event handler",
|
||||
)
|
||||
|
||||
@unicall.local(name="onComponentSizeChange")
|
||||
async def _on_component_size_change(
|
||||
self, component_id: int, new_width: float, new_height: float
|
||||
) -> None:
|
||||
"""
|
||||
Called by the client when a component is resized.
|
||||
"""
|
||||
component = self._weak_components_by_id[component_id]
|
||||
|
||||
for handler, _ in component._rio_event_handlers_[
|
||||
rio.event.EventTag.ON_RESIZE
|
||||
]:
|
||||
# Since the whole point of this event is to fetch data and
|
||||
# modify the component's state, wait for it to finish if it's
|
||||
# synchronous.
|
||||
self._call_event_handler_sync(
|
||||
handler, component, new_width, new_height
|
||||
)
|
||||
|
||||
@unicall.local(name="onFullscreenChange")
|
||||
async def _on_fullscreen_change(self, fullscreen: bool) -> None:
|
||||
"""
|
||||
|
||||
74
tests/test_frontend/test_layouting/test_on_resize.py
Normal file
74
tests/test_frontend/test_layouting/test_on_resize.py
Normal file
@@ -0,0 +1,74 @@
|
||||
import rio
|
||||
from tests.utils.layouting import verify_layout
|
||||
|
||||
recorded_events = []
|
||||
|
||||
|
||||
class MyComponent(rio.Component):
|
||||
@rio.event.on_resize
|
||||
def handle_resize(self, width, height) -> None:
|
||||
global recorded_events
|
||||
print(f"Component resized to {width}x{height}")
|
||||
recorded_events.append((width, height))
|
||||
|
||||
def build(self):
|
||||
return rio.Rectangle(
|
||||
fill=rio.Color.BLUE,
|
||||
min_width=5.0,
|
||||
min_height=10.0,
|
||||
)
|
||||
|
||||
|
||||
async def test_size_observer_reports_content_dimensions():
|
||||
global recorded_events
|
||||
|
||||
# Compute layout
|
||||
layout = await verify_layout(MyComponent)
|
||||
|
||||
# Find components
|
||||
session = layout.session
|
||||
size_observer = next(
|
||||
c
|
||||
for c in session._weak_components_by_id.values()
|
||||
if isinstance(c, MyComponent)
|
||||
)
|
||||
rectangle = next(
|
||||
c
|
||||
for c in session._weak_components_by_id.values()
|
||||
if isinstance(c, rio.Rectangle)
|
||||
)
|
||||
|
||||
observer_layout = layout._layouts_are[size_observer._id_]
|
||||
rectangle_layout = layout._layouts_are[rectangle._id_]
|
||||
|
||||
print(f"Observer layout: {observer_layout}")
|
||||
print(f"Rectangle layout: {rectangle_layout}")
|
||||
|
||||
# Verify layout dimensions
|
||||
observer_width = observer_layout.allocated_outer_width
|
||||
observer_height = observer_layout.allocated_outer_height
|
||||
rect_width = rectangle_layout.allocated_outer_width
|
||||
rect_height = rectangle_layout.allocated_outer_height
|
||||
|
||||
assert observer_width == rect_width, (
|
||||
f"Widths do not match: observer={observer_width}, rectangle={rect_width}"
|
||||
)
|
||||
assert observer_height == rect_height, (
|
||||
f"Heights do not match: observer={observer_height}, rectangle={rect_height}"
|
||||
)
|
||||
|
||||
print(
|
||||
f"Observer dimensions: {observer_width}x{observer_height},{session.pixels_per_font_height=}"
|
||||
)
|
||||
|
||||
# Verify size event
|
||||
assert len(recorded_events) >= 1, "Expected at least one resize event"
|
||||
observed_width = recorded_events[-1][0]
|
||||
observed_height = recorded_events[-1][1]
|
||||
|
||||
assert observed_width == observer_width, (
|
||||
f"Expected width {observer_width}, got {observed_width}"
|
||||
)
|
||||
assert observed_height == observer_height, (
|
||||
f"Expected height {observer_height}, got {observed_height}"
|
||||
)
|
||||
Reference in New Issue
Block a user