implement resize observer for component size changes

This commit is contained in:
ilya-pevzner
2025-04-09 12:25:32 -04:00
committed by iap
parent 41918fc625
commit e2c7abc5dd
5 changed files with 144 additions and 2 deletions

View File

@@ -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(

View File

@@ -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

View File

@@ -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

View File

@@ -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:
"""

View 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}"
)