Skip to content

Commit

Permalink
Use border and padding from Taffy (#96)
Browse files Browse the repository at this point in the history
* Update Taffy requirement to 0.5.2

* Fix paths

* Use border and padding as provided from Taffy

* Minor fixes

* Upgrade action versions

* Adjust test fixture rounding decimals
  • Loading branch information
mortencombat committed Jul 10, 2024
1 parent 1137ca5 commit 2244514
Show file tree
Hide file tree
Showing 6 changed files with 115 additions and 74 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ jobs:
name: Run tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
uses: actions/setup-python@v5
with:
python-version: 3.11
- name: Install ChromeDriver
Expand All @@ -21,7 +21,7 @@ jobs:
run: pytest --cov-report xml:reports/coverage.xml --cov=stretchable --junit-xml reports/pytest.xml --html=reports/pytest.html --self-contained-html tests/
continue-on-error: true
- name: Upload test results
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: test-results
path: reports/*
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,4 @@ pyo3 = { version = "0.20.0", features = ["abi3-py38", "extension-module"] }
dict_derive = "0.5.0"
log = "0.4"
pyo3-log = ">=0.9.0, <1.0"
taffy = ">=0.5.1, <0.6"
taffy = ">=0.5.2, <0.6"
15 changes: 4 additions & 11 deletions run_fixture.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,9 @@ def print_chrome_layout(node: WebElement, index: int = 0):
"""

filepath = Path(
# "/Users/kenneth/Code/Personal/Python/stretchable/tests/fixtures/flex/percentage_moderate_complexity.html"
# "/Users/kenneth/Code/Personal/Python/stretchable/tests/fixtures/grid/grid_margins_percent_start.html"
# "/Users/kenneth/Code/Personal/Python/stretchable/tests/fixtures/grid/grid_max_content_single_item_span_2_gap_fixed.html"
# "/Users/kenneth/Code/Personal/Python/stretchable/tests/fixtures/flex/gap_percentage_row_gap_wrapping.html"
# "/Users/kenneth/Code/Personal/Python/stretchable/tests/fixtures/flex/percentage_padding_should_calculate_based_only_on_width.html"
# "/Users/kenneth/Code/Personal/Python/stretchable/tests/fixtures/taffy/max_height_overrides_height_on_root.html"
# "/Users/kenneth/Code/Personal/Python/stretchable/tests/fixtures/taffy/min_height_overrides_height_on_root.html"
# "/Users/kenneth/Code/Personal/Python/stretchable/tests/fixtures/taffy/undefined_height_with_min_max.html"
"/Users/kenneth/Code/Personal/stretchable/tests/fixtures/block/block_overflow_scrollbars_overridden_by_available_space.html"
)
"./tests/fixtures/flex/percentage_padding_should_calculate_based_only_on_width.html"
# "./tests/fixtures/block/block_overflow_scrollbars_overridden_by_available_space.html"
).resolve()

# Get layout using taffy
xml = get_xml(filepath)
Expand All @@ -54,7 +47,7 @@ def print_chrome_layout(node: WebElement, index: int = 0):

# Get layout using Chrome
driver = webdriver.Chrome()
driver.get("file://" + str(filepath))
driver.get(f"file://{filepath}")
driver.implicitly_wait(0.5)
node_expected = driver.find_element(by=By.ID, value="test-root")
print("*** EXPECTED ***")
Expand Down
55 changes: 47 additions & 8 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -756,20 +756,59 @@ fn node_compute_layout_with_measure(taffy: usize, node: usize, available_space:
#[derive(FromPyObject, IntoPyObject)]
pub struct PyLayout {
order: i64,
left: f32,
top: f32,
width: f32,
height: f32,
location: Vec<f32>,
size: Vec<f32>,
content_size: Vec<f32>,
scrollbar_size: Vec<f32>,
border: Vec<f32>,
padding: Vec<f32>,
// margin: Vec<f32>,
}

trait FromPoint<T> {
fn from_point(value: taffy::geometry::Point<T>) -> Vec<T>;
}

impl<T> FromPoint<T> for Vec<T> {
fn from_point(value: taffy::geometry::Point<T>) -> Self {
vec![ value.x, value.y ]
}
}


trait FromSize<T> {
fn from_size(value: taffy::geometry::Size<T>) -> Vec<T>;
}

impl<T> FromSize<T> for Vec<T> {
fn from_size(value: taffy::geometry::Size<T>) -> Self {
vec![ value.width, value.height ]
}
}


trait FromRect<T> {
fn from_rect(value: taffy::geometry::Rect<T>) -> Vec<T>;
}

impl<T> FromRect<T> for Vec<T> {
fn from_rect(value: taffy::geometry::Rect<T>) -> Self {
vec![ value.top, value.right, value.bottom, value.left ]
}
}


impl From<Layout> for PyLayout {
fn from(layout: Layout) -> Self {
PyLayout {
order: layout.order as i64,
left: layout.location.x,
top: layout.location.y,
width: layout.size.width,
height: layout.size.height,
location: Vec::from_point(layout.location),
size: Vec::from_size(layout.size),
content_size: Vec::from_size(layout.content_size),
scrollbar_size: Vec::from_size(layout.scrollbar_size),
border: Vec::from_rect(layout.border),
padding: Vec::from_rect(layout.padding),
// margin: Vec::from_rect(layout.margin),
}
}
}
Expand Down
90 changes: 44 additions & 46 deletions src/stretchable/node.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import logging
import re
from enum import StrEnum, auto
from typing import Callable, Iterable, Optional, Self, SupportsIndex, Protocol
from typing import Callable, Iterable, Optional, Protocol, Self, SupportsIndex
from xml.etree import ElementTree

import attrs
Expand Down Expand Up @@ -44,7 +44,7 @@ def _measure_callback(
"""This function is a wrapper for the user-supplied measure function,
converting arguments into and results from the call by Taffy."""
if not context or context not in nodes:
return SizePoints((0, 0))
return (0, 0)

node = nodes[context]

Expand All @@ -61,6 +61,7 @@ def _measure_callback(
result.height.value if result.height else NAN,
)


class Edge(StrEnum):
"""Describes which edge of a node a given :py:obj:`Box` corresponds to. See the :doc:`glossary` for a description of the box model and the different boxes."""

Expand Down Expand Up @@ -91,6 +92,18 @@ class Box:
width: float
height: float

def _inset(self, insets: tuple[float, float, float, float]) -> Box:
"""
Returns a copy of the frame inset by the specified ``insets`` which must
be absolute values (floats).
"""
return Box(
self.x + insets[3],
self.y + insets[0],
self.width - insets[1] - insets[3],
self.height - insets[0] - insets[2],
)

def _offset(
self,
offsets: Rect,
Expand Down Expand Up @@ -592,20 +605,28 @@ def _update_layout(self) -> None:

layout = taffylib.node_get_layout(taffy._ptr, self._ptr)

# Update the border box and clear cached boxes for other edges
box = Box(layout["left"], layout["top"], layout["width"], layout["height"])
self._box = {Edge.BORDER: box}
self._zorder = layout["order"]

# Border box
box = Box(*layout["location"], *layout["size"])
self._box = {Edge.BORDER: box}

# Padding box (border box inset by borders)
box = box._inset(layout["border"])
self._box[Edge.PADDING] = box

# Content box (padding box inset by padding)
box = box._inset(layout["padding"])
self._box[Edge.CONTENT] = box

logger.debug(
"node_get_layout(t)affy: %s, node: %s) -> (order: %s, left: %s, top: %s, width: %s, height: %s)",
"node_get_layout(taffy: %s, node: %s) -> %s, border: %s, padding: %s, content: %s",
taffy._ptr,
self._ptr,
self._zorder,
box.x,
box.y,
box.width,
box.height,
layout,
self._box[Edge.BORDER],
self._box[Edge.PADDING],
self._box[Edge.CONTENT],
)

if self.is_visible:
Expand Down Expand Up @@ -668,42 +689,21 @@ def get_box(
if self.is_dirty:
raise LayoutNotComputedError

box = self.border_box
if edge == Edge.BORDER and relative and not flip_y:
return box
if relative and not flip_y and edge in self._box:
return self._box[edge]

# TODO: Consider implementing a caching mechanism for relative and/or flip_y
# h = hash((edge, relative, flip_y))

if USE_ROOT_CONTAINER and self.is_root and edge == Edge.MARGIN:
box = self._container.border_box
elif edge != Edge.BORDER:
# Expand or contract:
# Edge.CONTENT: -border -padding
# Edge.PADDING: -border
# Edge.BORDER: (none)
# Edge.MARGIN: +margin
# Padding, border and margin are defined in Style.

if edge in self._box:
box = self._box[edge]
else:
if edge == Edge.CONTENT:
actions = (
(self.style.border, -1),
(self.style.padding, -1),
)
elif edge == Edge.PADDING:
actions = ((self.style.border, -1),)
elif edge == Edge.MARGIN:
actions = ((self.style.margin, 1),)

box_parent = self._parent.get_box(Edge.BORDER) if self._parent else None
for offsets, factor in actions:
box = box._offset(offsets, box_parent, factor=factor)

self._box[edge] = box

# TODO: Consider implementing a caching mechanism for relative and/or flip_y
elif edge == Edge.MARGIN and Edge.MARGIN not in self._box:
# Taffy does not provide margin box, calculate it
box_parent = self._parent.get_box(Edge.BORDER) if self._parent else None
box = self.border_box._offset(self.style.margin, box_parent)
self._box[Edge.MARGIN] = box
else:
box = self._box[edge]

if not relative and self._parent:
box_parent = self._parent.get_box(Edge.BORDER, relative=False)
Expand Down Expand Up @@ -786,10 +786,8 @@ def _update_layout(self) -> None:
# NOTE: Since this container node has no margins, border and padding, this layout corresponds to all the boxes.

layout = taffylib.node_get_layout(taffy._ptr, self._ptr)
x = layout["left"]
y = layout["top"]
width = layout["width"]
height = layout["height"]
x, y = layout["location"]
width, height = layout["size"]
logger.debug(
"node_get_layout(taffy: %s, node: %s [container]) -> (left: %s, top: %s, width: %s, height: %s)",
taffy._ptr,
Expand Down
21 changes: 16 additions & 5 deletions tests/test_fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
H_HEIGHT: float = 10.0
ZERO_WIDTH_SPACE: str = "\u200b"
XML_REPLACE = (("&ZeroWidthSpace;", ZERO_WIDTH_SPACE),)
USE_ROUNDING: bool = False
NUM_DECIMALS: int = 0

"""
DEBUGGING NOTES:
Expand Down Expand Up @@ -103,15 +105,20 @@ def test_html_fixtures(driver: webdriver.Chrome, filepath: Path):
# Use Node.from_xml() to turn into node instances and compute layout with stretchable.
req_measure = requires_measure(ElementTree.fromstring(xml))
node = Node.from_xml(xml, apply_node_measure) if req_measure else Node.from_xml(xml)
node.compute_layout()
node.compute_layout(use_rounding=USE_ROUNDING)

# Render html with Chrome
driver.get("file://" + str(filepath))
driver.implicitly_wait(0.5)
node_expected = driver.find_element(by=By.ID, value="test-root")

# Compare rect of Chrome render with stretchable computed layout.
assert_node_layout(node, node_expected, filepath.stem)
assert_node_layout(
node,
node_expected,
filepath.stem,
num_decimals=0 if USE_ROUNDING else NUM_DECIMALS,
)


def get_xml(filepath: Path) -> str:
Expand Down Expand Up @@ -177,6 +184,8 @@ def assert_node_layout(
node_actual: Node,
node_expected: WebElement,
fixture: str,
*,
num_decimals: int = 1,
) -> None:
visible = node_expected.is_displayed()
assert (
Expand All @@ -196,9 +205,9 @@ def assert_node_layout(
for param in ("x", "y", "width", "height"):
v_act = round(
getattr(rect_actual, param),
1 if param == "x" or param == "y" else 0,
num_decimals if (param == "x" or param == "y") else 0,
)
v_exp = round(getattr(rect_expected, param), 1)
v_exp = round(getattr(rect_expected, param), num_decimals)

assert (
v_act == v_exp
Expand All @@ -215,7 +224,9 @@ def assert_node_layout(
assert (
child_expected.tag_name == "div"
), "Only <div> elements are supported in test fixtures"
assert_node_layout(child_actual, child_expected, f"{fixture}/{i}")
assert_node_layout(
child_actual, child_expected, f"{fixture}/{i}", num_decimals=num_decimals
)


def requires_measure(element: ElementTree.Element) -> bool:
Expand Down

0 comments on commit 2244514

Please sign in to comment.