add more details to layout explainer

This commit is contained in:
Aran-Fey
2025-03-02 17:53:11 +01:00
parent 040b0e4092
commit 60bee96ed0
14 changed files with 147 additions and 102 deletions

View File

@@ -156,7 +156,7 @@ class ComponentMeta(RioDataclassMeta):
component._session_ = session
# Create a unique ID for this component
component._id = session._next_free_component_id
component._id_ = session._next_free_component_id
session._next_free_component_id += 1
component._properties_assigned_after_creation_ = set()
@@ -188,7 +188,7 @@ class ComponentMeta(RioDataclassMeta):
#
# Components must be known by their id, so any messages addressed to
# them can be passed on correctly.
session._weak_components_by_id[component._id] = component
session._weak_components_by_id[component._id_] = component
session._register_dirty_component(
component,

View File

@@ -198,9 +198,9 @@ class Button(Component):
if isinstance(self.content, str):
content = f"text:{self.content!r}"
else:
content = f"content:{self.content._id}"
content = f"content:{self.content._id_}"
return f"<Button id:{self._id} {content}>"
return f"<Button id:{self._id_} {content}>"
class _ButtonInternal(FundamentalComponent):

View File

@@ -233,7 +233,7 @@ class Component(abc.ABC, metaclass=ComponentMeta):
margin_y: float | None = None
margin: float | None = None
_id: int = internal_field(init=False)
_id_: int = internal_field(init=False)
# Weak reference to the component's builder. Used to check if the component
# is still part of the component tree.
@@ -562,7 +562,7 @@ class Component(abc.ABC, metaclass=ComponentMeta):
else:
result = (
owning_component._is_in_component_tree_(cache)
and self._id in owning_component._owned_dialogs_
and self._id_ in owning_component._owned_dialogs_
)
# Cache the result and return
@@ -678,11 +678,11 @@ class Component(abc.ABC, metaclass=ComponentMeta):
return result
def __repr__(self) -> str:
result = f"<{type(self).__name__} id:{self._id}"
result = f"<{type(self).__name__} id:{self._id_}"
child_strings: list[str] = []
for child in self._iter_direct_children_():
child_strings.append(f" {type(child).__name__}:{child._id}")
child_strings.append(f" {type(child).__name__}:{child._id_}")
if child_strings:
result += " -" + "".join(child_strings)

View File

@@ -196,4 +196,4 @@ class KeyboardFocusableFundamentalComponent(FundamentalComponent):
`public`: False
"""
await self.session._remote_set_keyboard_focus(self._id)
await self.session._remote_set_keyboard_focus(self._id_)

View File

@@ -275,7 +275,7 @@ class Grid(FundamentalComponent):
def _custom_serialize_(self) -> JsonDoc:
return {
"_children": [child._id for child in self._children],
"_children": [child._id_ for child in self._children],
"_child_positions": [vars(pos) for pos in self._child_positions],
}

View File

@@ -69,7 +69,7 @@ class ScrollTarget(FundamentalComponent):
button_content = self.copy_button_content
return {
"copy_button_content": button_content._id
"copy_button_content": button_content._id_
if isinstance(button_content, rio.Component)
else None,
"copy_button_text": button_content

View File

@@ -454,7 +454,7 @@ class Table(FundamentalComponent): # TODO: add more content to docstring
"headers": self._headers,
"columns": self._columns,
"styling": [style._serialize(session) for style in self._styling],
"children": [child._id for child in self._children],
"children": [child._id_ for child in self._children],
"childPositions": self._child_positions,
} # type: ignore

View File

@@ -161,7 +161,7 @@ class Text(FundamentalComponent):
else:
text = self.text
return f"<{type(self).__name__} id:{self._id} text:{text!r}>"
return f"<{type(self).__name__} id:{self._id_} text:{text!r}>"
Text._unique_id_ = "Text-builtin"

View File

@@ -103,7 +103,7 @@ class LayoutExplainer:
self.increase_height = []
# Fetch layout information. This is asynchronous
(self._layout,) = await session._get_component_layouts([component._id])
(self._layout,) = await session._get_component_layouts([component._id_])
if self._layout.parent_id is None:
self._parent = None
@@ -343,31 +343,65 @@ class LayoutExplainer:
result.write(
f"This matches the {axis_name} needed by the component."
)
elif alignment is None:
result.write(
"Because no alignment is set, it uses all of that space."
)
else:
result.write("\n\n")
result.write(
f"Due to `align_{axis_xy}` being set, the {target_class_name} only takes up the minimum amount of space necessary"
f"This is more than its natural {axis_name} of {natural_size:.1f}."
)
if alignment <= 0.03:
if alignment is None:
result.write(
f" and is located at the {start} of the available space."
)
elif 0.47 <= alignment <= 0.53:
result.write(f" and is centered in the available space.")
elif alignment >= 0.97:
result.write(
f" and is located at the {end} of the available space."
" Because no alignment is set, it uses all of that space."
)
else:
result.write(
f", with {alignment * 100:.0f}% of the leftover space on the {start}, and the remainder on the {end}."
f"\n\nDue to `align_{axis_xy}` being set, the component only takes up the minimum amount of space necessary"
)
if alignment <= 0.03:
result.write(
f" and is located at the {start} of the available space."
)
elif 0.47 <= alignment <= 0.53:
result.write(f" and is centered in the available space.")
elif alignment >= 0.97:
result.write(
f" and is located at the {end} of the available space."
)
else:
result.write(
f", with {alignment * 100:.0f}% of the leftover space on the {start}, and the remainder on the {end}."
)
# If the component is at its minimum size or aligned, and it has
# multiple children, explain which child is responsible for the
# component's size
if (
allocated_size_before_alignment < natural_size + total_margin + 0.1
or alignment is not None
):
if (
isinstance(self.component, rio.Row) and axis_name == "height"
) or (
isinstance(self.component, rio.Column) and axis_name == "width"
):
children = self.component.children
if len(children) > 1:
child_layouts = await self.session._get_component_layouts(
[child._id_ for child in children]
)
largest_child_index = max(
range(len(children)),
key=lambda index: getattr(
child_layouts[index], f"requested_outer_{axis_name}"
),
)
largest_child = children[largest_child_index]
largest_child_layout = child_layouts[largest_child_index]
result.write(
f"\n\nThe largest child is the {number_to_rank(largest_child_index + 1)} one (a `{type(largest_child).__name__}`), with a {axis_name} of {getattr(largest_child_layout, f'requested_outer_{axis_name}'):,.1f}. This is what determined the component's natural {axis_name}."
)
# Warn if the specified minimum size is less than the natural one
if 0 < specified_min_size < natural_size:
self.warnings.append(
@@ -429,3 +463,14 @@ class LayoutExplainer:
if isinstance(self.component, rio.Text):
if axis_name == "width":
yield 'Set `overflow="wrap"` or `overflow="ellipsize"` to reduce the `Text`\'s natural width'
def number_to_rank(number: int) -> str:
if number == 1:
return "first"
elif number == 2:
return "second"
elif number == 3:
return "third"
else:
return f"{number}th"

View File

@@ -277,7 +277,7 @@ class Layouter:
# Make sure the received components match expectations
component_ids_client = set(self._layouts_are.keys())
component_ids_server = {c._id for c in self._ordered_components}
component_ids_server = {c._id_ for c in self._ordered_components}
missing_component_ids = component_ids_server - component_ids_client
assert not missing_component_ids, missing_component_ids
@@ -323,7 +323,7 @@ class Layouter:
for component in self._ordered_components:
if component.key == key:
return self._layouts_should[component._id]
return self._layouts_should[component._id_]
raise KeyError(f"There is no component with key `{key}`")
@@ -354,7 +354,7 @@ class Layouter:
#
# 1. Update natural & requested width
for component in reversed(ordered_components):
layout_is = self._layouts_are[component._id]
layout_is = self._layouts_are[component._id_]
layout_should = UnittestComponentLayout(
natural_width=-1,
natural_height=-1,
@@ -373,7 +373,7 @@ class Layouter:
parent_id=layout_is.parent_id,
aux={},
)
self._layouts_should[component._id] = layout_should
self._layouts_should[component._id_] = layout_should
# Let the component update its natural width
self._update_natural_width(component)
@@ -398,7 +398,7 @@ class Layouter:
# 2. Update allocated width
for component in ordered_components:
layout = self._layouts_should[component._id]
layout = self._layouts_should[component._id_]
left, width = calculate_alignment(
allocated_outer_size=layout.allocated_outer_width,
@@ -414,7 +414,7 @@ class Layouter:
# 3. Update natural & requested height
for component in reversed(ordered_components):
layout_should = self._layouts_should[component._id]
layout_should = self._layouts_should[component._id_]
# Let the component update its natural height
self._update_natural_height(component)
@@ -438,7 +438,7 @@ class Layouter:
# 4. Update allocated height
for component in ordered_components:
layout = self._layouts_should[component._id]
layout = self._layouts_should[component._id_]
top, height = calculate_alignment(
allocated_outer_size=layout.allocated_outer_height,
@@ -463,8 +463,8 @@ class Layouter:
"""
# Default implementation: Trust the client
layout_should = self._layouts_should[component._id]
layout_is = self._layouts_are[component._id]
layout_should = self._layouts_should[component._id_]
layout_is = self._layouts_are[component._id_]
layout_should.natural_width = layout_is.natural_width
@@ -473,12 +473,12 @@ class Layouter:
component: rio.Row,
) -> None:
# Prepare
layout = self._layouts_should[component._id]
layout = self._layouts_should[component._id_]
child_widths: list[float] = []
for child in component._iter_direct_children_():
child_layout = self._layouts_should[child._id]
child_layout = self._layouts_should[child._id_]
child_widths.append(child_layout.requested_outer_width)
# Update
@@ -493,11 +493,11 @@ class Layouter:
component: rio.Column,
) -> None:
# Max of all Children
layout = self._layouts_should[component._id]
layout = self._layouts_should[component._id_]
layout.natural_width = 0
for child in component._iter_direct_children_():
child_layout = self._layouts_should[child._id]
child_layout = self._layouts_should[child._id_]
layout.natural_width = max(
layout.natural_width, child_layout.requested_outer_width
)
@@ -506,7 +506,7 @@ class Layouter:
self,
component: rio.Overlay,
) -> None:
layout = self._layouts_should[component._id]
layout = self._layouts_should[component._id_]
layout.natural_width = 0
def _update_natural_width_SingleContainer(
@@ -514,12 +514,12 @@ class Layouter:
component: rio.Component,
) -> None:
# Prepare
layout = self._layouts_should[component._id]
layout = self._layouts_should[component._id_]
layout.natural_width = 0
# Pass on all space
for child in iter_direct_tree_children(component):
child_layout = self._layouts_should[child._id]
child_layout = self._layouts_should[child._id_]
layout.natural_width = max(
layout.natural_width,
child_layout.requested_outer_width,
@@ -539,8 +539,8 @@ class Layouter:
"""
# Default implementation: Trust the client
for child in iter_direct_tree_children(component):
child_layout_should = self._layouts_should[child._id]
child_layout_is = self._layouts_are[child._id]
child_layout_should = self._layouts_should[child._id_]
child_layout_is = self._layouts_are[child._id_]
child_layout_should.left_in_viewport_outer = (
child_layout_is.left_in_viewport_outer
@@ -556,8 +556,8 @@ class Layouter:
) -> 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]
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
@@ -576,12 +576,12 @@ class Layouter:
component: rio.Row,
) -> None:
# Prepare
layout = self._layouts_should[component._id]
layout = self._layouts_should[component._id_]
child_widths: list[float] = []
for child in component._iter_direct_children_():
child_layout = self._layouts_should[child._id]
child_layout = self._layouts_should[child._id_]
child_widths.append(child_layout.requested_outer_width)
# Update
@@ -598,7 +598,7 @@ class Layouter:
for child, (left, width) in zip(
component._iter_direct_children_(), starts_and_sizes
):
child_layout = self._layouts_should[child._id]
child_layout = self._layouts_should[child._id_]
child_layout.left_in_viewport_outer = (
layout.left_in_viewport_inner + left
)
@@ -608,10 +608,10 @@ class Layouter:
self,
component: rio.Column,
) -> None:
layout = self._layouts_should[component._id]
layout = self._layouts_should[component._id_]
for child in component._iter_direct_children_():
child_layout = self._layouts_should[child._id]
child_layout = self._layouts_should[child._id_]
child_layout.left_in_viewport_outer = layout.left_in_viewport_inner
child_layout.allocated_outer_width = layout.allocated_inner_width
@@ -619,7 +619,7 @@ class Layouter:
self,
component: rio.Overlay,
) -> None:
child_layout = self._layouts_should[component.content._id]
child_layout = self._layouts_should[component.content._id_]
child_layout.left_in_viewport_outer = 0
child_layout.allocated_outer_width = self.window_width
@@ -628,11 +628,11 @@ class Layouter:
component: rio.Component,
) -> None:
# Prepare
layout = self._layouts_should[component._id]
layout = self._layouts_should[component._id_]
# Pass on all space
for child in iter_direct_tree_children(component):
child_layout = self._layouts_should[child._id]
child_layout = self._layouts_should[child._id_]
child_layout.left_in_viewport_outer = layout.left_in_viewport_inner
child_layout.allocated_outer_width = layout.allocated_inner_width
@@ -646,8 +646,8 @@ class Layouter:
all children already have their requested height set.
"""
# Default implementation: Trust the client
layout_should = self._layouts_should[component._id]
layout_is = self._layouts_are[component._id]
layout_should = self._layouts_should[component._id_]
layout_is = self._layouts_are[component._id_]
layout_should.natural_height = layout_is.natural_height
@@ -656,11 +656,11 @@ class Layouter:
component: rio.Row,
) -> None:
# Max of all Children
layout = self._layouts_should[component._id]
layout = self._layouts_should[component._id_]
layout.natural_height = 0
for child in component._iter_direct_children_():
child_layout = self._layouts_should[child._id]
child_layout = self._layouts_should[child._id_]
layout.natural_height = max(
layout.natural_height, child_layout.requested_outer_height
)
@@ -670,12 +670,12 @@ class Layouter:
component: rio.Column,
) -> None:
# Prepare
layout = self._layouts_should[component._id]
layout = self._layouts_should[component._id_]
child_heights: list[float] = []
for child in component._iter_direct_children_():
child_layout = self._layouts_should[child._id]
child_layout = self._layouts_should[child._id_]
child_heights.append(child_layout.requested_outer_height)
# Update
@@ -689,7 +689,7 @@ class Layouter:
self,
component: rio.Overlay,
) -> None:
layout = self._layouts_should[component._id]
layout = self._layouts_should[component._id_]
layout.natural_height = 0
def _update_natural_height_SingleContainer(
@@ -697,12 +697,12 @@ class Layouter:
component: rio.Component,
) -> None:
# Prepare
layout = self._layouts_should[component._id]
layout = self._layouts_should[component._id_]
layout.natural_height = 0
# Pass on all space
for child in iter_direct_tree_children(component):
child_layout = self._layouts_should[child._id]
child_layout = self._layouts_should[child._id_]
layout.natural_height = max(
layout.natural_height,
child_layout.requested_outer_height,
@@ -722,8 +722,8 @@ class Layouter:
"""
# Default implementation: Trust the client
for child in iter_direct_tree_children(component):
child_layout_should = self._layouts_should[child._id]
child_layout_is = self._layouts_are[child._id]
child_layout_should = self._layouts_should[child._id_]
child_layout_is = self._layouts_are[child._id_]
child_layout_should.allocated_outer_height = (
child_layout_is.allocated_outer_height
@@ -739,8 +739,8 @@ class Layouter:
) -> 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]
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
@@ -758,10 +758,10 @@ class Layouter:
self,
component: rio.Row,
) -> None:
layout = self._layouts_should[component._id]
layout = self._layouts_should[component._id_]
for child in component._iter_direct_children_():
child_layout = self._layouts_should[child._id]
child_layout = self._layouts_should[child._id_]
child_layout.top_in_viewport_outer = layout.top_in_viewport_inner
child_layout.allocated_outer_height = layout.allocated_inner_height
@@ -770,12 +770,12 @@ class Layouter:
component: rio.Column,
) -> None:
# Prepare
layout = self._layouts_should[component._id]
layout = self._layouts_should[component._id_]
child_heights: list[float] = []
for child in component._iter_direct_children_():
child_layout = self._layouts_should[child._id]
child_layout = self._layouts_should[child._id_]
child_heights.append(child_layout.requested_outer_height)
# Update
@@ -792,7 +792,7 @@ class Layouter:
for child, (top, height) in zip(
component._iter_direct_children_(), starts_and_sizes
):
child_layout = self._layouts_should[child._id]
child_layout = self._layouts_should[child._id_]
child_layout.top_in_viewport_outer = (
layout.top_in_viewport_inner + top
)
@@ -802,7 +802,7 @@ class Layouter:
self,
component: rio.Overlay,
) -> None:
child_layout = self._layouts_should[component.content._id]
child_layout = self._layouts_should[component.content._id_]
child_layout.top_in_viewport_outer = 0
child_layout.allocated_outer_height = self.window_height
@@ -811,11 +811,11 @@ class Layouter:
component: rio.Component,
) -> None:
# Prepare
layout = self._layouts_should[component._id]
layout = self._layouts_should[component._id_]
# Pass on all space
for child in iter_direct_tree_children(component):
child_layout = self._layouts_should[child._id]
child_layout = self._layouts_should[child._id_]
child_layout.top_in_viewport_outer = layout.top_in_viewport_inner
child_layout.allocated_outer_height = layout.allocated_inner_height
@@ -841,9 +841,9 @@ class Layouter:
return
# Build the subresult
layout = layouts[component._id]
layout = layouts[component._id_]
value_json = {
"id": component._id,
"id": component._id_,
"type": type(component).__name__,
**uniserde.as_json(layout),
}
@@ -921,7 +921,7 @@ class Layouter:
color = (color_8bit, color_8bit, color_8bit)
# Draw the component
layout = layouts[component._id]
layout = layouts[component._id_]
rect_left = layout.left_in_viewport_inner * pixels_per_unit
rect_top = layout.top_in_viewport_inner * pixels_per_unit

View File

@@ -96,7 +96,7 @@ class Dialog(t.Generic[T]):
# Try to remove the dialog from its owning component. This cannot fail,
# since the code above guards against closing the dialog multiple times.
del self._owning_component._owned_dialogs_[self._root_component._id]
del self._owning_component._owned_dialogs_[self._root_component._id_]
# Done!
return True
@@ -125,7 +125,7 @@ class Dialog(t.Generic[T]):
# The dialog was just discarded on the Python side. Tell the client to
# also remove it.
await self._root_component.session._remove_dialog(
self._root_component._id,
self._root_component._id_,
)
@property

View File

@@ -156,14 +156,14 @@ def serialize_and_host_component(component: rio.Component) -> JsonDoc:
# -> Pretend it's a fundamental component
elif isinstance(component, rio.components.dialog_container.DialogContainer):
result["_type_"] = "DialogContainer-builtin"
result["content"] = component._build_data_.build_result._id # type: ignore
result["content"] = component._build_data_.build_result._id_ # type: ignore
result.update(component.serialize())
else:
# Take care to add underscores to any properties here, as the
# user-defined state is also added and could clash
result["_type_"] = "HighLevelComponent-builtin"
result["_child_"] = component._build_data_.build_result._id # type: ignore
result["_child_"] = component._build_data_.build_result._id_ # type: ignore
return result
@@ -189,9 +189,9 @@ def get_attribute_serializers(
for attr_name, field in class_local_fields(cls).items():
if not field.serialize:
assert (
attr_name not in serializers
), f"A base class wants to serialize {attr_name}, but {cls} doesn't"
assert attr_name not in serializers, (
f"A base class wants to serialize {attr_name}, but {cls} doesn't"
)
continue
serializer = _get_serializer_for_annotation(annotations[attr_name])
@@ -218,7 +218,7 @@ def _serialize_self_serializing(
def _serialize_child_component(
sess: session.Session, component: rio.Component
) -> Jsonable:
return component._id
return component._id_
def _serialize_sequence(

View File

@@ -1404,7 +1404,7 @@ window.location.href = {json.dumps(str(active_page_url))};
# Serialize all components which have been visited
delta_states: dict[int, JsonDoc] = {
component._id: serialization.serialize_and_host_component(
component._id_: serialization.serialize_and_host_component(
component
)
for component in visited_components
@@ -1469,7 +1469,7 @@ window.location.href = {json.dumps(str(active_page_url))};
# send the high level root component. JS only cares about the
# fundamental one.
if self._high_level_root_component in visited_components:
del delta_states[self._high_level_root_component._id]
del delta_states[self._high_level_root_component._id_]
root_build: BuildData = self._high_level_root_component._build_data_ # type: ignore
fundamental_root_component = root_build.build_result
@@ -1477,7 +1477,7 @@ window.location.href = {json.dumps(str(active_page_url))};
fundamental_root_component,
fundamental_component.FundamentalComponent,
), fundamental_root_component
root_component_id = fundamental_root_component._id
root_component_id = fundamental_root_component._id_
else:
root_component_id = None
@@ -1499,7 +1499,7 @@ window.location.href = {json.dumps(str(active_page_url))};
component
) in self._high_level_root_component._iter_component_tree_():
visited_components.add(component)
delta_states[component._id] = (
delta_states[component._id_] = (
serialization.serialize_and_host_component(component)
)
@@ -2882,7 +2882,7 @@ a.remove();
dialog_container = dialog_container.DialogContainer(
build_content=build,
owning_component_id=owning_component._id,
owning_component_id=owning_component._id_,
is_modal=modal,
is_user_closable=user_closable,
on_close=on_close,
@@ -2901,7 +2901,7 @@ a.remove();
# Register the dialog with the component. This keeps it (and contained
# components) alive until the component is destroyed.
owning_component._owned_dialogs_[dialog_container._id] = result
owning_component._owned_dialogs_[dialog_container._id_] = result
# Refresh. This will build any components in the dialog and send them to
# the client
@@ -3760,12 +3760,12 @@ a.remove();
rio.components.root_components.FundamentalRootComponent,
), ll_root_component
ll_layout = result.component_layouts[ll_root_component._id]
ll_layout = result.component_layouts[ll_root_component._id_]
hl_layout = copy.deepcopy(ll_layout)
hl_layout.aux = {}
result.component_layouts[hl_root_component._id] = hl_layout
result.component_layouts[hl_root_component._id_] = hl_layout
ll_layout.parent_id = hl_root_component._id
ll_layout.parent_id = hl_root_component._id_
# Done
return result

View File

@@ -174,7 +174,7 @@ class TestClient:
]: delta
for component_id, delta in delta_states.items()
if int(component_id)
!= self.session._high_level_root_component._id
!= self.session._high_level_root_component._id_
}
return {}
@@ -187,9 +187,9 @@ class TestClient:
result = component._build_data_.build_result # type: ignore
if type_ is not None:
assert (
type(result) is type_
), f"Expected {type_}, got {type(result)}"
assert type(result) is type_, (
f"Expected {type_}, got {type(result)}"
)
return result # type: ignore