fix allocation of HighLevelRootComponent

This commit is contained in:
Aran-Fey
2024-12-17 12:36:43 +01:00
parent d46d671ba1
commit 492075a1bc
10 changed files with 116 additions and 54 deletions

View File

@@ -466,7 +466,7 @@ export async function processMessageReturnResponse(
break;
case "getUnittestClientLayoutInfo":
response = getUnittestClientLayoutInfo();
response = await getUnittestClientLayoutInfo();
break;
case "removeDialog":

View File

@@ -1,4 +1,4 @@
import { getComponentLayout } from "./utils";
import { getComponentLayout, sleep } from "./utils";
import { pixelsPerRem } from "./app";
import {
componentsById,
@@ -289,7 +289,14 @@ function dumpComponentRecursively(
}
}
export function getUnittestClientLayoutInfo(): UnittestClientLayoutInfo {
export async function getUnittestClientLayoutInfo(): Promise<UnittestClientLayoutInfo> {
// This function is only used by rio's unit tests. We know it runs after the
// first `updateComponentStates`, so we don't need to worry about that. But,
// since layouting includes quite a few `requestAnimationFrame`s and
// `resizeObserver`s, some layouts may still be in flux. We'll wait a little
// while before we fetch the layouts.
await sleep(0.1);
// Prepare the result
const result = {} as UnittestClientLayoutInfo;

View File

@@ -454,11 +454,15 @@ export function getComponentLayout(component: ComponentBase): ComponentLayout {
result.topInViewportInner = innerRect.top / pixelsPerRem;
// Allocated size
result.allocatedOuterWidth = outerRect.width / pixelsPerRem;
result.allocatedOuterHeight = outerRect.height / pixelsPerRem;
result.allocatedOuterWidth =
getAllocatedWidthInPx(outerElement) / pixelsPerRem;
result.allocatedOuterHeight =
getAllocatedHeightInPx(outerElement) / pixelsPerRem;
result.allocatedInnerWidth = innerRect.width / pixelsPerRem;
result.allocatedInnerHeight = innerRect.height / pixelsPerRem;
result.allocatedInnerWidth =
getAllocatedWidthInPx(innerElement) / pixelsPerRem;
result.allocatedInnerHeight =
getAllocatedHeightInPx(innerElement) / pixelsPerRem;
let naturalSizeInPixels = getNaturalSizeInPixels(innerElement);
result.naturalWidth = naturalSizeInPixels[0] / pixelsPerRem;

View File

@@ -1,5 +1,6 @@
from __future__ import annotations
import functools
import json
import sys
import typing as t
@@ -44,23 +45,25 @@ def specialized(func: t.Callable[P, R]) -> t.Callable[P, R]:
"""
def result(self, component, *args, **kwargs) -> t.Any:
# Special case: A lot of containers behave in the same way - they pass
# on all space. Avoid having to implement them all separately.
if type(component) in FULL_SIZE_SINGLE_CONTAINERS or not isinstance(
component,
rio.components.fundamental_component.FundamentalComponent,
):
function_name = f"{func.__name__}_SingleContainer"
else:
function_name = f"{func.__name__}_{type(component).__name__}"
# Delegate to a specialized method if it exists
# First, check if a specialized method for this component exists
try:
method = getattr(self, function_name)
method = getattr(
self,
f"{func.__name__}_{type(component).__name__}",
)
except AttributeError:
return func(self, component, *args, **kwargs) # type: ignore
else:
return method(component, *args, **kwargs)
# Special case: A lot of containers behave in the same way - they pass
# on all space. Avoid having to implement them all separately.
if type(component) in FULL_SIZE_SINGLE_CONTAINERS or not isinstance(
component,
rio.components.fundamental_component.FundamentalComponent,
):
method = getattr(self, f"{func.__name__}_SingleContainer")
else:
# If all else fails, use the default implementation
method = functools.partial(func, self)
return method(component, *args, **kwargs) # type: ignore (wtf?)
return result # type: ignore
@@ -234,7 +237,8 @@ class Layouter:
# before its children.
_ordered_components: list[rio.Component]
# Construction of this class is asynchronous. Make sure nobody does anything silly.
# Construction of this class is asynchronous. Make sure nobody does anything
# silly.
def __init__(self) -> None:
raise TypeError(
"Creating this class is asynchronous. Use the `create` method instead."
@@ -393,14 +397,6 @@ class Layouter:
)
# 2. Update allocated width
root_layout = self._layouts_should[
self.session._high_level_root_component._id
]
root_layout.left_in_viewport_outer = 0
root_layout.allocated_outer_width = max(
self.window_width, root_layout.requested_outer_width
)
for component in ordered_components:
layout = self._layouts_should[component._id]
@@ -441,11 +437,6 @@ class Layouter:
)
# 4. Update allocated height
root_layout.top_in_viewport_outer = 0
root_layout.allocated_outer_height = max(
self.window_height, root_layout.requested_outer_height
)
for component in ordered_components:
layout = self._layouts_should[component._id]
@@ -559,6 +550,27 @@ class Layouter:
child_layout_is.allocated_outer_width
)
def _update_allocated_width_HighLevelRootComponent(
self,
component: rio.components.root_components.HighLevelRootComponent,
) -> None:
# Since the HighLevelRootComponent doesn't have a parent, it has to set
# its own allocation
layout_should = self._layouts_should[component._id]
layout_is = self._layouts_are[component._id]
# Because scrolling differs between debug mode and release mode (user
# content scrolls vs browser scrolls), we'll just copy the values from
# the client.
layout_should.left_in_viewport_outer = layout_is.left_in_viewport_outer
layout_should.left_in_viewport_inner = layout_is.left_in_viewport_inner
layout_should.allocated_outer_width = layout_is.allocated_outer_width
layout_should.allocated_inner_width = layout_is.allocated_inner_width
# Then behave like a regular SingleContainer
self._update_allocated_width_SingleContainer(component)
def _update_allocated_width_Row(
self,
component: rio.Row,
@@ -721,6 +733,27 @@ class Layouter:
child_layout_is.top_in_viewport_outer
)
def _update_allocated_height_HighLevelRootComponent(
self,
component: rio.components.root_components.HighLevelRootComponent,
) -> None:
# Since the HighLevelRootComponent doesn't have a parent, it has to set
# its own allocation
layout_should = self._layouts_should[component._id]
layout_is = self._layouts_are[component._id]
# Because scrolling differs between debug mode and release mode (user
# content scrolls vs browser scrolls), we'll just copy the values from
# the client.
layout_should.top_in_viewport_outer = layout_is.top_in_viewport_outer
layout_should.top_in_viewport_inner = layout_is.top_in_viewport_inner
layout_should.allocated_outer_height = layout_is.allocated_outer_height
layout_should.allocated_inner_height = layout_is.allocated_inner_height
# Then behave like a regular SingleContainer
self._update_allocated_height_SingleContainer(component)
def _update_allocated_height_Row(
self,
component: rio.Row,

View File

@@ -3539,6 +3539,8 @@ a.remove();
hl_layout.aux = {}
result.component_layouts[hl_root_component._id] = hl_layout
ll_layout.parent_id = hl_root_component._id
# Done
return result

0
tests/__init__.py Normal file
View File

View File

@@ -5,9 +5,9 @@ import typing as t
import imy.docstrings
import pytest
from utils.ruff import ruff_check, ruff_format # type: ignore
import rio.docs
from tests.utils.ruff import ruff_check, ruff_format
def parametrize_with_name(

View File

@@ -1,13 +1,26 @@
import typing as t
import pytest
from utils.layouting import cleanup, setup, verify_layout # type: ignore
import rio.data_models
import rio.debug.layouter
import rio.testing
from tests.utils.layouting import cleanup, setup, verify_layout
# pytestmark = pytest.mark.async_timeout(30)
# For debugging. Set this to a number > 0 if you want to look at the browser.
#
# Note: Chrome's console doesn't show `console.debug` messages per default. To
# see them, click on "All levels" and check "Verbose".
DEBUG_SHOW_BROWSER_DURATION = 0
if DEBUG_SHOW_BROWSER_DURATION:
pytestmark = pytest.mark.async_timeout(DEBUG_SHOW_BROWSER_DURATION + 30)
import tests.utils.layouting
tests.utils.layouting.DEBUG_EXTRA_SLEEP_DURATION = (
DEBUG_SHOW_BROWSER_DURATION
)
@pytest.fixture(scope="module", autouse=True)

View File

@@ -62,7 +62,7 @@ PAGES = [
@pytest.mark.parametrize(
"relative_url_before_redirects,relative_url_after_redirects_should",
"relative_url_before_redirects, relative_url_after_redirects_should",
[
# No redirects
(
@@ -158,9 +158,3 @@ def test_redirects(
assert (
absolute_url_after_redirects_is == absolute_url_after_redirects_should
)
test_redirects(
"/guard-to-page-1",
"/page-1",
)

View File

@@ -4,7 +4,6 @@ import asyncio
import typing as t
import playwright.async_api
import playwright.sync_api
import uvicorn
import rio.app_server
@@ -18,6 +17,9 @@ from rio.utils import choose_free_port
__all__ = ["verify_layout", "setup", "cleanup"]
DEBUG_EXTRA_SLEEP_DURATION = 0
layouter_factory: LayouterFactory | None = None
@@ -132,11 +134,14 @@ class LayouterFactory:
self._app._build = build
session, page = await self._create_session()
# FIXME: Give the client some time to process the layout
await asyncio.sleep(0.5)
layouter = await Layouter.create(session)
# Sleep to keep the browser open for debugging. We do this *after*
# obtaining the component layouts so that the
# `getUnittestClientLayoutInfo` response can be seen in the browser
# console.
await asyncio.sleep(DEBUG_EXTRA_SLEEP_DURATION)
await page.close()
await session._close(close_remote_session=False)
@@ -189,7 +194,9 @@ class LayouterFactory:
playwright_obj = await self._playwright_context.__aenter__()
try:
browser = await playwright_obj.chromium.launch()
browser = await playwright_obj.chromium.launch(
headless=DEBUG_EXTRA_SLEEP_DURATION == 0
)
except Exception:
raise Exception(
"Playwright cannot launch chromium. Please execute the"
@@ -200,9 +207,11 @@ class LayouterFactory:
# With default settings, playwright gets detected as a crawler. So we
# need to emulate a real device.
self._browser = await browser.new_context(
**playwright_obj.devices["Desktop Chrome"]
)
kwargs = dict(playwright_obj.devices["Desktop Chrome"])
# The default window size is too large to fit on my screen, which sucks
# when debugging. Make it smaller.
kwargs["viewport"] = {"width": 800, "height": 600}
self._browser = await browser.new_context(**kwargs)
async def _create_session(self) -> tuple[Session, t.Any]:
assert (