@@ -35,6 +35,33 @@ impl<'a> PipelineTableTree<'a> {
3535 }
3636 }
3737
38+ /// Sum of the border-box heights of the nested tables directly contained in a cell (not
39+ /// counting tables nested deeper inside those). Zero if the cell holds no table. This lets
40+ /// a table cell grow to contain a nested table whose height lattice computes in a later pass.
41+ fn nested_table_height ( & self , cell_layout_id : LayoutElementId ) -> f32 {
42+ let Some ( el) = self . layout_tree . arena . get ( & cell_layout_id) else {
43+ return 0.0 ;
44+ } ;
45+ let mut total = 0.0 ;
46+ for & child_id in & el. children {
47+ let Some ( child) = self . layout_tree . arena . get ( & child_id) else {
48+ continue ;
49+ } ;
50+ let is_table = matches ! (
51+ self . doc. get_own_style( child. dom_node_id, & StyleProperty :: Display ) ,
52+ Some ( Value :: Display ( Display :: Table ) )
53+ ) ;
54+ if is_table {
55+ // Self-contained nested table — stop here, don't double-count its inner tables.
56+ total += child. box_model . border_box . height as f32 ;
57+ } else {
58+ // The table may be wrapped (e.g. in an anonymous box); keep descending.
59+ total += self . nested_table_height ( child_id) ;
60+ }
61+ }
62+ total
63+ }
64+
3865 /// Convert pending relative positions to absolute `BoxModel`s in the arena.
3966 /// Must be called after `compute_table_layout` returns.
4067 pub fn apply_positions ( & mut self , table_dom_id : DomNodeId ) {
@@ -153,8 +180,10 @@ fn cell_layout_to_box_model(layout: &CellLayout, abs: Coordinate) -> BoxModel {
153180fn intrinsic_content_width ( el : & LayoutElementNode , arena : & HashMap < LayoutElementId , LayoutElementNode > ) -> f32 {
154181 match & el. context {
155182 ElementContext :: Text ( _) => el. box_model . content_box . width as f32 ,
156- ElementContext :: Image ( ctx) => ctx. dimension . width as f32 ,
157- ElementContext :: Svg ( ctx) => ctx. dimension . width as f32 ,
183+ // Replaced elements: use the laid-out border-box width so the column is wide enough for
184+ // the image *including its own CSS border* (the bare `dimension` omits it). Images are
185+ // never stretched to the cell width, so the border box is the true intrinsic width.
186+ ElementContext :: Image ( _) | ElementContext :: Svg ( _) => el. box_model . border_box . width as f32 ,
158187 ElementContext :: None => el
159188 . children
160189 . iter ( )
@@ -239,7 +268,11 @@ impl TableTree for PipelineTableTree<'_> {
239268 // than 0 and covers the most common case (single column of text).
240269 if let Some ( & layout_id) = self . dom_to_layout . get ( & id) {
241270 if let Some ( element) = self . layout_tree . arena . get ( & layout_id) {
242- return element. box_model . content_box . height as f32 ;
271+ let taffy_h = element. box_model . content_box . height as f32 ;
272+ // A cell containing a nested table must be at least as tall as that table.
273+ // The nested table's real height is only known after lattice lays it out, so
274+ // the second (bottom-up) pass in `post_process_tables` propagates it up here.
275+ return taffy_h. max ( self . nested_table_height ( layout_id) ) ;
243276 }
244277 }
245278 0.0
@@ -248,7 +281,10 @@ impl TableTree for PipelineTableTree<'_> {
248281 fn cell_content_width ( & self , id : DomNodeId ) -> f32 {
249282 if let Some ( & layout_id) = self . dom_to_layout . get ( & id) {
250283 if let Some ( element) = self . layout_tree . arena . get ( & layout_id) {
251- return intrinsic_content_width ( element, & self . layout_tree . arena ) ;
284+ // Include the cell's own horizontal padding so the column is wide enough to hold
285+ // the content *and* its padding (e.g. HN's logo cell: 20px image + 4px padding-right).
286+ let pad = ( element. box_model . padding . left + element. box_model . padding . right ) as f32 ;
287+ return intrinsic_content_width ( element, & self . layout_tree . arena ) + pad;
252288 }
253289 }
254290 0.0
@@ -273,59 +309,69 @@ pub fn post_process_tables(layout_tree: &mut LayoutTree, dom_to_layout: &HashMap
273309
274310 log:: info!( "lattice: post_process_tables found {} table node(s)" , table_nodes. len( ) ) ;
275311
276- for ( table_dom_id, table_layout_id) in table_nodes {
277- // Use the parent element's content width as available_width. For nested
278- // tables the parent is a table cell whose box model was already updated
279- // by the outer table's apply_positions call, giving us the correct width.
280- // Fall back to the table's own Taffy-computed width for root-level tables.
281- let available_width = doc
282- . parent ( table_dom_id)
283- . and_then ( |p| dom_to_layout. get ( & p) )
284- . and_then ( |& pid| layout_tree. arena . get ( & pid) )
285- . map ( |el| el. box_model . content_box . width as f32 )
286- . unwrap_or_else ( || {
287- layout_tree
288- . arena
289- . get ( & table_layout_id)
290- . map ( |e| e. box_model . content_box . width as f32 )
291- . unwrap_or ( 0.0 )
292- } ) ;
312+ // Two passes. Pass 1 is pre-order (outer→inner): it establishes column widths, which flow
313+ // top-down (a nested table reads its width from its already-sized parent cell). Pass 2 is
314+ // post-order (inner→outer): each table is re-laid-out *after* the tables nested inside its
315+ // cells, so an outer cell's height now reflects its nested table's true height — height
316+ // flows bottom-up. A single reverse pass propagates through any table-nesting depth.
317+ for pass in 0 ..2 {
318+ let order: Vec < ( DomNodeId , LayoutElementId ) > = if pass == 0 {
319+ table_nodes. clone ( )
320+ } else {
321+ table_nodes. iter ( ) . rev ( ) . copied ( ) . collect ( )
322+ } ;
323+ for ( table_dom_id, table_layout_id) in order {
324+ lay_out_one_table ( & * doc, layout_tree, dom_to_layout, table_dom_id, table_layout_id) ;
325+ }
326+ }
327+ }
293328
294- log:: info!(
295- "lattice: computing layout for table node {:?}, available_width={}" ,
296- table_dom_id,
297- available_width
298- ) ;
329+ /// Run lattice for a single table node and write the computed cell positions and the table's
330+ /// own size back into the layout tree.
331+ fn lay_out_one_table (
332+ doc : & dyn PipelineDocument ,
333+ layout_tree : & mut LayoutTree ,
334+ dom_to_layout : & HashMap < DomNodeId , LayoutElementId > ,
335+ table_dom_id : DomNodeId ,
336+ table_layout_id : LayoutElementId ,
337+ ) {
338+ // Use the parent element's content width as available_width. For nested
339+ // tables the parent is a table cell whose box model was already updated
340+ // by the outer table's apply_positions call, giving us the correct width.
341+ // Fall back to the table's own Taffy-computed width for root-level tables.
342+ let available_width = doc
343+ . parent ( table_dom_id)
344+ . and_then ( |p| dom_to_layout. get ( & p) )
345+ . and_then ( |& pid| layout_tree. arena . get ( & pid) )
346+ . map ( |el| el. box_model . content_box . width as f32 )
347+ . unwrap_or_else ( || {
348+ layout_tree
349+ . arena
350+ . get ( & table_layout_id)
351+ . map ( |e| e. box_model . content_box . width as f32 )
352+ . unwrap_or ( 0.0 )
353+ } ) ;
299354
300- // &*doc borrows from our local Arc, not from layout_tree — no conflict.
301- let mut tree = PipelineTableTree :: new ( & * doc, layout_tree, dom_to_layout) ;
355+ let mut tree = PipelineTableTree :: new ( doc, layout_tree, dom_to_layout) ;
302356
303- match gosub_lattice:: compute_table_layout ( & mut tree, table_dom_id, available_width, None ) {
304- Ok ( ( table_width, table_height) ) => {
305- log:: info!(
306- "lattice: table node {:?} → {}×{}px, pending={}" ,
307- table_dom_id,
308- table_width,
309- table_height,
310- tree. pending. len( )
357+ match gosub_lattice:: compute_table_layout ( & mut tree, table_dom_id, available_width, None ) {
358+ Ok ( ( table_width, table_height) ) => {
359+ tree. apply_positions ( table_dom_id) ;
360+ // Write back both dimensions so deeply-nested tables can read the
361+ // correct width from this table's box model via their parent lookup.
362+ if let Some ( el) = layout_tree. arena . get_mut ( & table_layout_id) {
363+ let bb = el. box_model . border_box ;
364+ el. box_model = BoxModel :: new (
365+ Rect :: new ( bb. x , bb. y , table_width as f64 , table_height as f64 ) ,
366+ el. box_model . padding ,
367+ el. box_model . border ,
368+ el. box_model . margin ,
311369 ) ;
312- tree. apply_positions ( table_dom_id) ;
313- // Write back both dimensions so deeply-nested tables can read the
314- // correct width from this table's box model via their parent lookup.
315- if let Some ( el) = layout_tree. arena . get_mut ( & table_layout_id) {
316- let bb = el. box_model . border_box ;
317- el. box_model = BoxModel :: new (
318- Rect :: new ( bb. x , bb. y , table_width as f64 , table_height as f64 ) ,
319- el. box_model . padding ,
320- el. box_model . border ,
321- el. box_model . margin ,
322- ) ;
323- }
324- }
325- Err ( e) => {
326- log:: warn!( "lattice: table layout failed for node {:?}: {:?}" , table_dom_id, e) ;
327370 }
328371 }
372+ Err ( e) => {
373+ log:: warn!( "lattice: table layout failed for node {:?}: {:?}" , table_dom_id, e) ;
374+ }
329375 }
330376}
331377
0 commit comments