From 24b00a39d0dbf995e8dc410e5a232d348aa3aa74 Mon Sep 17 00:00:00 2001 From: Brent Rowland Date: Thu, 30 Apr 2026 07:58:36 -0700 Subject: [PATCH 1/5] Remodel Dimensions to track specified and resolved sizes --- ltml/dimensions.go | 110 ++++++++++++++++++----- ltml/dimensions_test.go | 168 ++++++++++++++++++++++++++++------- ltml/layout_overflow_test.go | 14 ++- ltml/std_container.go | 16 +--- ltml/std_index.go | 10 +-- ltml/std_paragraph.go | 5 +- ltml/std_widget.go | 30 +++---- 7 files changed, 252 insertions(+), 101 deletions(-) diff --git a/ltml/dimensions.go b/ltml/dimensions.go index aaed8de..e1ab79c 100644 --- a/ltml/dimensions.go +++ b/ltml/dimensions.go @@ -9,19 +9,37 @@ import ( "strconv" ) +type DimensionMode int8 + +const ( + DimUnspecified DimensionMode = iota + DimSpecified + DimPct + DimRel +) + type Dimensions struct { - sides Sides - margin Sides - padding Sides - corners Corners - width float32 - widthPct float32 - widthRel float32 - height float32 - heightPct float32 - heightRel float32 - widthSet bool - heightSet bool + sides Sides + margin Sides + padding Sides + corners Corners + width float32 + height float32 + widthValue float32 + heightValue float32 + widthMode DimensionMode + heightMode DimensionMode +} + +type dimensionState struct { + resolved float32 + value float32 + mode DimensionMode +} + +type dimensionsState struct { + width dimensionState + height dimensionState } var ( @@ -109,19 +127,31 @@ func (d *Dimensions) SetAttrs(attrs map[string]string, units Units) { } func (d *Dimensions) SetHeight(value float64) { - d.height, d.heightPct, d.heightRel, d.heightSet = float32(value), 0, 0, true + d.height = float32(value) + d.heightValue = float32(value) + d.heightMode = DimSpecified +} + +func (d *Dimensions) ClearHeight() { + d.height = 0 + d.heightValue = 0 + d.heightMode = DimUnspecified } func (d *Dimensions) SetHeightPct(value float64) { - d.heightPct, d.height, d.heightRel, d.heightSet = float32(value), 0, 0, true + d.height = 0 + d.heightValue = float32(value) + d.heightMode = DimPct } func (d *Dimensions) SetHeightRel(value float64) { - d.heightRel, d.height, d.heightPct, d.heightSet = float32(value), 0, 0, true + d.height = 0 + d.heightValue = float32(value) + d.heightMode = DimRel } func (d *Dimensions) HeightIsSet() bool { - return d.heightSet + return d.heightMode != DimUnspecified } func (d *Dimensions) SetTop(value float64) { @@ -141,15 +171,27 @@ func (d *Dimensions) SetLeft(value float64) { } func (d *Dimensions) SetWidth(value float64) { - d.width, d.widthPct, d.widthRel, d.widthSet = float32(value), 0, 0, true + d.width = float32(value) + d.widthValue = float32(value) + d.widthMode = DimSpecified +} + +func (d *Dimensions) ClearWidth() { + d.width = 0 + d.widthValue = 0 + d.widthMode = DimUnspecified } func (d *Dimensions) SetWidthPct(value float64) { - d.widthPct, d.widthRel, d.width, d.widthSet = float32(value), 0, 0, true + d.width = 0 + d.widthValue = float32(value) + d.widthMode = DimPct } func (d *Dimensions) SetWidthRel(value float64) { - d.widthRel, d.widthPct, d.width, d.widthSet = float32(value), 0, 0, true + d.width = 0 + d.widthValue = float32(value) + d.widthMode = DimRel } func (d *Dimensions) String() string { @@ -158,13 +200,37 @@ func (d *Dimensions) String() string { } func (d *Dimensions) WidthPctIsSet() bool { - return d.widthPct > 0 + return d.widthMode == DimPct } func (d *Dimensions) WidthRelIsSet() bool { - return d.widthRel != 0 + return d.widthMode == DimRel } func (d *Dimensions) WidthIsSet() bool { - return d.widthSet + return d.widthMode != DimUnspecified +} + +func (d *Dimensions) SaveState() dimensionsState { + return dimensionsState{ + width: dimensionState{ + resolved: d.width, + value: d.widthValue, + mode: d.widthMode, + }, + height: dimensionState{ + resolved: d.height, + value: d.heightValue, + mode: d.heightMode, + }, + } +} + +func (d *Dimensions) RestoreState(state dimensionsState) { + d.width = state.width.resolved + d.widthValue = state.width.value + d.widthMode = state.width.mode + d.height = state.height.resolved + d.heightValue = state.height.value + d.heightMode = state.height.mode } diff --git a/ltml/dimensions_test.go b/ltml/dimensions_test.go index 939f3ad..c29cd2f 100644 --- a/ltml/dimensions_test.go +++ b/ltml/dimensions_test.go @@ -9,25 +9,25 @@ import ( func TestDimensions_SetAttrs(t *testing.T) { tests := []struct { - name string - attrs map[string]string - wantWidth float64 - wantWidthPct float64 - wantWidthRel float64 - wantHeight float64 - wantHeightPct float64 - wantHeightRel float64 - wantWidthSet bool - wantHeightSet bool + name string + attrs map[string]string + wantWidth float64 + wantWidthValue float64 + wantWidthMode DimensionMode + wantHeight float64 + wantHeightValue float64 + wantHeightMode DimensionMode + wantWidthSet bool + wantHeightSet bool }{ - {name: "Width", attrs: map[string]string{"width": "30"}, wantWidth: 30, wantWidthSet: true}, - {name: "WidthPct", attrs: map[string]string{"width": "40%"}, wantWidthPct: 40, wantWidthSet: true}, - {name: "WidthRelPlus", attrs: map[string]string{"width": "+50"}, wantWidthRel: 50, wantWidthSet: true}, - {name: "WidthRelMinus", attrs: map[string]string{"width": "-60"}, wantWidthRel: -60, wantWidthSet: true}, - {name: "Height", attrs: map[string]string{"height": "30"}, wantHeight: 30, wantHeightSet: true}, - {name: "HeightPct", attrs: map[string]string{"height": "40%"}, wantHeightPct: 40, wantHeightSet: true}, - {name: "HeightRelPlus", attrs: map[string]string{"height": "+50"}, wantHeightRel: 50, wantHeightSet: true}, - {name: "HeightRelMinus", attrs: map[string]string{"height": "-60"}, wantHeightRel: -60, wantHeightSet: true}, + {name: "Width", attrs: map[string]string{"width": "30"}, wantWidth: 30, wantWidthValue: 30, wantWidthMode: DimSpecified, wantWidthSet: true}, + {name: "WidthPct", attrs: map[string]string{"width": "40%"}, wantWidthValue: 40, wantWidthMode: DimPct, wantWidthSet: true}, + {name: "WidthRelPlus", attrs: map[string]string{"width": "+50"}, wantWidthValue: 50, wantWidthMode: DimRel, wantWidthSet: true}, + {name: "WidthRelMinus", attrs: map[string]string{"width": "-60"}, wantWidthValue: -60, wantWidthMode: DimRel, wantWidthSet: true}, + {name: "Height", attrs: map[string]string{"height": "30"}, wantHeight: 30, wantHeightValue: 30, wantHeightMode: DimSpecified, wantHeightSet: true}, + {name: "HeightPct", attrs: map[string]string{"height": "40%"}, wantHeightValue: 40, wantHeightMode: DimPct, wantHeightSet: true}, + {name: "HeightRelPlus", attrs: map[string]string{"height": "+50"}, wantHeightValue: 50, wantHeightMode: DimRel, wantHeightSet: true}, + {name: "HeightRelMinus", attrs: map[string]string{"height": "-60"}, wantHeightValue: -60, wantHeightMode: DimRel, wantHeightSet: true}, } for _, tc := range tests { @@ -39,27 +39,135 @@ func TestDimensions_SetAttrs(t *testing.T) { if got := float64(d.width); got != tc.wantWidth { t.Errorf("width: expected %v, got %v", tc.wantWidth, got) } - if got := float64(d.widthPct); got != tc.wantWidthPct { - t.Errorf("widthPct: expected %v, got %v", tc.wantWidthPct, got) + if got := float64(d.widthValue); got != tc.wantWidthValue { + t.Errorf("widthValue: expected %v, got %v", tc.wantWidthValue, got) } - if got := float64(d.widthRel); got != tc.wantWidthRel { - t.Errorf("widthRel: expected %v, got %v", tc.wantWidthRel, got) + if d.widthMode != tc.wantWidthMode { + t.Errorf("widthMode: expected %v, got %v", tc.wantWidthMode, d.widthMode) } if got := float64(d.height); got != tc.wantHeight { t.Errorf("height: expected %v, got %v", tc.wantHeight, got) } - if got := float64(d.heightPct); got != tc.wantHeightPct { - t.Errorf("heightPct: expected %v, got %v", tc.wantHeightPct, got) + if got := float64(d.heightValue); got != tc.wantHeightValue { + t.Errorf("heightValue: expected %v, got %v", tc.wantHeightValue, got) } - if got := float64(d.heightRel); got != tc.wantHeightRel { - t.Errorf("heightRel: expected %v, got %v", tc.wantHeightRel, got) + if d.heightMode != tc.wantHeightMode { + t.Errorf("heightMode: expected %v, got %v", tc.wantHeightMode, d.heightMode) } - if d.widthSet != tc.wantWidthSet { - t.Errorf("widthSet: expected %v, got %v", tc.wantWidthSet, d.widthSet) + if got := d.WidthIsSet(); got != tc.wantWidthSet { + t.Errorf("WidthIsSet: expected %v, got %v", tc.wantWidthSet, got) } - if d.heightSet != tc.wantHeightSet { - t.Errorf("heightSet: expected %v, got %v", tc.wantHeightSet, d.heightSet) + if got := d.HeightIsSet(); got != tc.wantHeightSet { + t.Errorf("HeightIsSet: expected %v, got %v", tc.wantHeightSet, got) } }) } } + +func TestStdWidget_DimensionResolution(t *testing.T) { + page := &StdPage{pageStyle: &PageStyle{width: 200, height: 120}} + + pct := &StdWidget{} + _ = pct.SetContainer(page) + pct.SetWidthPct(25) + pct.SetHeightPct(50) + if got := pct.Width(); got != 50 { + t.Fatalf("pct.Width() = %v, want 50", got) + } + if got := pct.Height(); got != 60 { + t.Fatalf("pct.Height() = %v, want 60", got) + } + + rel := &StdWidget{} + _ = rel.SetContainer(page) + rel.SetWidthRel(-20) + rel.SetHeightRel(15) + if got := rel.Width(); got != 180 { + t.Fatalf("rel.Width() = %v, want 180", got) + } + if got := rel.Height(); got != 135 { + t.Fatalf("rel.Height() = %v, want 135", got) + } +} + +func TestDetectWidths_PreservesPercentClassification(t *testing.T) { + page := &StdPage{pageStyle: &PageStyle{width: 200, height: 120}} + grid := NewWidgetGrid(2, 1) + + pct := &StdWidget{} + _ = pct.SetContainer(page) + pct.SetWidthPct(40) + grid.SetCell(0, 0, pct) + + specified := &StdWidget{} + _ = specified.SetContainer(page) + specified.SetWidth(80) + grid.SetCell(1, 0, specified) + + widths := detectWidths(grid, nil) + if got := widths[0].How; got != Percent { + t.Fatalf("widths[0].How = %v, want %v", got, Percent) + } + if got := widths[0].Size; got != 80 { + t.Fatalf("widths[0].Size = %v, want 80", got) + } + if got := widths[1].How; got != Specified { + t.Fatalf("widths[1].How = %v, want %v", got, Specified) + } + if got := widths[1].Size; got != 80 { + t.Fatalf("widths[1].Size = %v, want 80", got) + } +} + +func TestStdIndex_ClearMeasuredGeometry_ClearsOnlyImplicitDimensions(t *testing.T) { + index := &StdIndex{} + index.width = 140 + index.widthValue = 25 + index.widthMode = DimPct + index.height = 90 + index.heightValue = 12 + index.heightMode = DimRel + index.clearMeasuredGeometry() + + if index.width != 0 || index.widthValue != 0 || index.widthMode != DimUnspecified { + t.Fatalf("implicit width not cleared: width=%v value=%v mode=%v", index.width, index.widthValue, index.widthMode) + } + if index.height != 0 || index.heightValue != 0 || index.heightMode != DimUnspecified { + t.Fatalf("implicit height not cleared: height=%v value=%v mode=%v", index.height, index.heightValue, index.heightMode) + } + + index.SetAttrs(map[string]string{"width": "40%", "height": "30"}) + index.width = 160 + index.height = 30 + index.clearMeasuredGeometry() + + if index.width != 160 || index.widthValue != 40 || index.widthMode != DimPct { + t.Fatalf("explicit width was not preserved: width=%v value=%v mode=%v", index.width, index.widthValue, index.widthMode) + } + if index.height != 30 || index.heightValue != 30 || index.heightMode != DimSpecified { + t.Fatalf("explicit height was not preserved: height=%v value=%v mode=%v", index.height, index.heightValue, index.heightMode) + } +} + +func TestDimensions_SaveStateAndClearHelpers(t *testing.T) { + var d Dimensions + d.SetWidthPct(40) + d.SetHeight(24) + + saved := d.SaveState() + d.ClearWidth() + d.ClearHeight() + + if d.WidthIsSet() || d.HeightIsSet() { + t.Fatalf("dimensions should be cleared, got widthMode=%v heightMode=%v", d.widthMode, d.heightMode) + } + + d.RestoreState(saved) + + if d.widthMode != DimPct || d.widthValue != 40 || d.width != 0 { + t.Fatalf("width state restore failed: mode=%v value=%v width=%v", d.widthMode, d.widthValue, d.width) + } + if d.heightMode != DimSpecified || d.heightValue != 24 || d.height != 24 { + t.Fatalf("height state restore failed: mode=%v value=%v height=%v", d.heightMode, d.heightValue, d.height) + } +} diff --git a/ltml/layout_overflow_test.go b/ltml/layout_overflow_test.go index d915479..071a76c 100644 --- a/ltml/layout_overflow_test.go +++ b/ltml/layout_overflow_test.go @@ -142,7 +142,7 @@ func TestLayoutVBox_DirectChildSplitTableUsesOuterOverflowInsteadOfSelfClipping( table.layout = defaultLayouts["table"].Clone() table.order = TableOrderRows table.cols = 2 - table.widthPct = 100 + table.SetWidthPct(100) table.splitEnabled = true table.splitExplicit = true table.headerRows = 1 @@ -597,8 +597,7 @@ func TestStdContainer_SplitForHeight_VBoxRepeatsHeadersAndFooters(t *testing.T) box := &StdContainer{} _ = box.SetContainer(page) box.layout = defaultLayouts["vbox"].Clone() - box.width = 180 - box.widthSet = true + box.SetWidth(180) page.AddChild(box) add := func(name string, height float64, align string) { @@ -649,8 +648,7 @@ func TestStdContainer_SplitForHeight_VBoxSplitsBodyParagraph(t *testing.T) { box := &StdContainer{} _ = box.SetContainer(page) box.layout = defaultLayouts["vbox"].Clone() - box.width = 120 - box.widthSet = true + box.SetWidth(120) page.AddChild(box) header := &flowTestWidget{name: "header", preferredHeight: 10} @@ -708,8 +706,7 @@ func TestStdContainer_SplitForHeight_VBoxReturnsNilWhenRepeatedChromeConsumesPag box := &StdContainer{} _ = box.SetContainer(page) box.layout = defaultLayouts["vbox"].Clone() - box.width = 180 - box.widthSet = true + box.SetWidth(180) page.AddChild(box) header := &flowTestWidget{name: "header", preferredHeight: 20} @@ -896,8 +893,7 @@ func TestStdContainer_SplitForHeight_TableRepeatsHeaderAndFooterRows(t *testing. table.layout = defaultLayouts["table"].Clone() table.order = TableOrderRows table.cols = 2 - table.width = 180 - table.widthSet = true + table.SetWidth(180) table.splitEnabled = true table.splitExplicit = true table.headerRows = 1 diff --git a/ltml/std_container.go b/ltml/std_container.go index 735e5d5..fc002db 100644 --- a/ltml/std_container.go +++ b/ltml/std_container.go @@ -115,12 +115,10 @@ func (c *StdContainer) PreferredHeight(w Writer) float64 { return height } } - savedHeight, savedHeightPct, savedHeightRel, savedHeightSet := - c.height, c.heightPct, c.heightRel, c.heightSet + saved := c.SaveState() LayoutContainer(c, newLayoutProbeWriter(w)) height := c.Height() - c.height, c.heightPct, c.heightRel, c.heightSet = - savedHeight, savedHeightPct, savedHeightRel, savedHeightSet + c.RestoreState(saved) return height } @@ -501,10 +499,7 @@ func (c *StdContainer) tableFragmentHeight(metrics *tableSplitMetrics, bodyStart func (c *StdContainer) cloneTableFragment(metrics *tableSplitMetrics, rows []int) *StdContainer { clone := *c clone.activeChildren = c.cloneTableWidgetsForRows(metrics.grid, rows, &clone) - clone.height = 0 - clone.heightPct = 0 - clone.heightRel = 0 - clone.heightSet = false + clone.ClearHeight() clone.printed = false clone.invisible = false clone.disabled = false @@ -853,10 +848,7 @@ func (c *StdContainer) vboxHasVisibleWidget(groups ...[]Widget) bool { func (c *StdContainer) cloneVBoxFragment(included map[Widget]bool, replacements map[Widget]Widget) *StdContainer { clone := *c clone.activeChildren = make([]Widget, 0, len(included)+len(replacements)) - clone.height = 0 - clone.heightPct = 0 - clone.heightRel = 0 - clone.heightSet = false + clone.ClearHeight() clone.printed = false clone.invisible = false clone.disabled = false diff --git a/ltml/std_index.go b/ltml/std_index.go index fe8b2be..774dfee 100644 --- a/ltml/std_index.go +++ b/ltml/std_index.go @@ -109,16 +109,10 @@ func (i *StdIndex) clearExpandedState() { func (i *StdIndex) clearMeasuredGeometry() { if !i.explicitWidth { - i.width = 0 - i.widthPct = 0 - i.widthRel = 0 - i.widthSet = false + i.ClearWidth() } if !i.explicitHeight { - i.height = 0 - i.heightPct = 0 - i.heightRel = 0 - i.heightSet = false + i.ClearHeight() } } diff --git a/ltml/std_paragraph.go b/ltml/std_paragraph.go index af6264d..42f23c7 100644 --- a/ltml/std_paragraph.go +++ b/ltml/std_paragraph.go @@ -272,10 +272,7 @@ func (p *StdParagraph) cloneForSplit(lines []*rich_text.RichText, suppressBullet clone.splitLines = append([]*rich_text.RichText(nil), lines...) clone.suppressBullet = suppressBullet clone.continuationIndent = continuationIndent - clone.height = 0 - clone.heightPct = 0 - clone.heightRel = 0 - clone.heightSet = false + clone.ClearHeight() clone.richText = nil clone.printed = false clone.invisible = false diff --git a/ltml/std_widget.go b/ltml/std_widget.go index 04dfd91..c67bf47 100644 --- a/ltml/std_widget.go +++ b/ltml/std_widget.go @@ -762,13 +762,12 @@ func (widget *StdWidget) Units() Units { } func (widget *StdWidget) Width() float64 { - if widget.widthPct > 0 { - return float64(widget.widthPct) / 100.0 * ContentWidth(widget.container) - } - if widget.widthRel != 0 { - return ContentWidth(widget.container) + float64(widget.widthRel) - } - if widget.widthSet { + switch widget.widthMode { + case DimPct: + return float64(widget.widthValue) / 100.0 * ContentWidth(widget.container) + case DimRel: + return ContentWidth(widget.container) + float64(widget.widthValue) + case DimSpecified: return float64(widget.width) } if widget.sides[leftSide].IsSet && widget.sides[rightSide].IsSet { @@ -778,13 +777,12 @@ func (widget *StdWidget) Width() float64 { } func (widget *StdWidget) Height() float64 { - if widget.heightPct > 0 { - return float64(widget.heightPct) / 100.0 * ContentHeight(widget.container) - } - if widget.heightRel != 0 { - return ContentHeight(widget.container) + float64(widget.heightRel) - } - if widget.heightSet { + switch widget.heightMode { + case DimPct: + return float64(widget.heightValue) / 100.0 * ContentHeight(widget.container) + case DimRel: + return ContentHeight(widget.container) + float64(widget.heightValue) + case DimSpecified: return float64(widget.height) } if widget.sides[topSide].IsSet && widget.sides[bottomSide].IsSet { @@ -794,11 +792,11 @@ func (widget *StdWidget) Height() float64 { } func (widget *StdWidget) HeightIsSet() bool { - return widget.heightSet || (widget.sides[topSide].IsSet && widget.sides[bottomSide].IsSet) + return widget.heightMode != DimUnspecified || (widget.sides[topSide].IsSet && widget.sides[bottomSide].IsSet) } func (widget *StdWidget) WidthIsSet() bool { - return widget.widthSet || (widget.sides[leftSide].IsSet && widget.sides[rightSide].IsSet) + return widget.widthMode != DimUnspecified || (widget.sides[leftSide].IsSet && widget.sides[rightSide].IsSet) } func (widget *StdWidget) Visible() bool { From bb2395f6d34b1cb0b26f094df6eb45abafe8b017 Mon Sep 17 00:00:00 2001 From: Brent Rowland Date: Thu, 30 Apr 2026 09:20:03 -0700 Subject: [PATCH 2/5] Add auto dimensions to LTML hbox layouts --- ltml/SYNTAX.md | 7 + ltml/dimensions.go | 46 +++- ltml/dimensions_test.go | 55 ++++- ltml/dir_test.go | 95 ------- ltml/layout_box.go | 52 +++- ltml/layout_box_test.go | 272 +++++++++++++++++++++ ltml/ltml_test.go | 1 + ltml/samples/test_056_hbox_auto_width.ltml | 76 ++++++ ltml/samples/test_056_hbox_auto_width.pdf | Bin 0 -> 34944 bytes ltml/std_widget.go | 4 +- ltml/widget.go | 4 + 11 files changed, 498 insertions(+), 114 deletions(-) create mode 100644 ltml/layout_box_test.go create mode 100644 ltml/samples/test_056_hbox_auto_width.ltml create mode 100644 ltml/samples/test_056_hbox_auto_width.pdf diff --git a/ltml/SYNTAX.md b/ltml/SYNTAX.md index c3e6362..6c8d16d 100644 --- a/ltml/SYNTAX.md +++ b/ltml/SYNTAX.md @@ -1178,6 +1178,12 @@ not automatically change paragraph shaping or bidi behavior inside text widgets. - Use `align-self="start"`, `align-self="center"`, or `align-self="end"` to control vertical placement within the hbox track. - In `hbox`, `align-self="start"` means top and `align-self="end"` means bottom. - Unaligned children share remaining width equally unless `width` is specified. +- When at least one child uses `width="auto"` and the hbox has true surplus + width beyond the preferred widths of the remaining unsized children plus + hpadding, omitted widths keep their preferred widths and the `auto` children + split the leftover space evenly. +- In constrained hboxes, and in other layout managers, `width="auto"` behaves + the same as an omitted width. - In `dir="rtl"`, stacking order reverses: children flow right to left, `align="left"` pins to the right side, and `align="right"` pins to the left side. ### Table Details @@ -1353,6 +1359,7 @@ Measurements can be expressed in several forms: | With unit | `1in`, `2.5cm`, `14pt` | Explicit unit overrides the current `units`. | | Percentage | `50%` | Percentage of the container's content width or height. | | Relative | `+10`, `-5` | Offset from the container's content dimension. | +| Auto | `auto` | Automatic layout-managed size. Currently special-cased only for `hbox` width; elsewhere it behaves like omitting the dimension. | **Supported units:** `pt` (points), `in` (inches, 72pt), `cm` (centimeters, 28.35pt). diff --git a/ltml/dimensions.go b/ltml/dimensions.go index e1ab79c..6e859b9 100644 --- a/ltml/dimensions.go +++ b/ltml/dimensions.go @@ -7,6 +7,7 @@ import ( "fmt" "regexp" "strconv" + "strings" ) type DimensionMode int8 @@ -16,6 +17,7 @@ const ( DimSpecified DimPct DimRel + DimAuto ) type Dimensions struct { @@ -101,7 +103,10 @@ func (d *Dimensions) SetAttrs(attrs map[string]string, units Units) { } if width, ok := attrs["width"]; ok { - if rePct.MatchString(width) { + width = strings.TrimSpace(width) + if width == "auto" { + d.SetWidthAuto() + } else if rePct.MatchString(width) { widthPct, _ := strconv.ParseFloat(width[:len(width)-1], 64) d.SetWidthPct(widthPct) } else if reRel.MatchString(width) { @@ -113,7 +118,10 @@ func (d *Dimensions) SetAttrs(attrs map[string]string, units Units) { } } if height, ok := attrs["height"]; ok { - if rePct.MatchString(height) { + height = strings.TrimSpace(height) + if height == "auto" { + d.SetHeightAuto() + } else if rePct.MatchString(height) { heightPct, _ := strconv.ParseFloat(height[:len(height)-1], 64) d.SetHeightPct(heightPct) } else if reRel.MatchString(height) { @@ -132,6 +140,12 @@ func (d *Dimensions) SetHeight(value float64) { d.heightMode = DimSpecified } +func (d *Dimensions) SetHeightAuto() { + d.height = 0 + d.heightValue = 0 + d.heightMode = DimAuto +} + func (d *Dimensions) ClearHeight() { d.height = 0 d.heightValue = 0 @@ -151,7 +165,12 @@ func (d *Dimensions) SetHeightRel(value float64) { } func (d *Dimensions) HeightIsSet() bool { - return d.heightMode != DimUnspecified + switch d.heightMode { + case DimSpecified, DimPct, DimRel: + return true + default: + return false + } } func (d *Dimensions) SetTop(value float64) { @@ -176,6 +195,12 @@ func (d *Dimensions) SetWidth(value float64) { d.widthMode = DimSpecified } +func (d *Dimensions) SetWidthAuto() { + d.width = 0 + d.widthValue = 0 + d.widthMode = DimAuto +} + func (d *Dimensions) ClearWidth() { d.width = 0 d.widthValue = 0 @@ -208,7 +233,20 @@ func (d *Dimensions) WidthRelIsSet() bool { } func (d *Dimensions) WidthIsSet() bool { - return d.widthMode != DimUnspecified + switch d.widthMode { + case DimSpecified, DimPct, DimRel: + return true + default: + return false + } +} + +func (d *Dimensions) WidthMode() DimensionMode { + return d.widthMode +} + +func (d *Dimensions) HeightMode() DimensionMode { + return d.heightMode } func (d *Dimensions) SaveState() dimensionsState { diff --git a/ltml/dimensions_test.go b/ltml/dimensions_test.go index c29cd2f..2e685ed 100644 --- a/ltml/dimensions_test.go +++ b/ltml/dimensions_test.go @@ -24,10 +24,12 @@ func TestDimensions_SetAttrs(t *testing.T) { {name: "WidthPct", attrs: map[string]string{"width": "40%"}, wantWidthValue: 40, wantWidthMode: DimPct, wantWidthSet: true}, {name: "WidthRelPlus", attrs: map[string]string{"width": "+50"}, wantWidthValue: 50, wantWidthMode: DimRel, wantWidthSet: true}, {name: "WidthRelMinus", attrs: map[string]string{"width": "-60"}, wantWidthValue: -60, wantWidthMode: DimRel, wantWidthSet: true}, + {name: "WidthAuto", attrs: map[string]string{"width": "auto"}, wantWidthMode: DimAuto}, {name: "Height", attrs: map[string]string{"height": "30"}, wantHeight: 30, wantHeightValue: 30, wantHeightMode: DimSpecified, wantHeightSet: true}, {name: "HeightPct", attrs: map[string]string{"height": "40%"}, wantHeightValue: 40, wantHeightMode: DimPct, wantHeightSet: true}, {name: "HeightRelPlus", attrs: map[string]string{"height": "+50"}, wantHeightValue: 50, wantHeightMode: DimRel, wantHeightSet: true}, {name: "HeightRelMinus", attrs: map[string]string{"height": "-60"}, wantHeightValue: -60, wantHeightMode: DimRel, wantHeightSet: true}, + {name: "HeightAuto", attrs: map[string]string{"height": "auto"}, wantHeightMode: DimAuto}, } for _, tc := range tests { @@ -42,8 +44,8 @@ func TestDimensions_SetAttrs(t *testing.T) { if got := float64(d.widthValue); got != tc.wantWidthValue { t.Errorf("widthValue: expected %v, got %v", tc.wantWidthValue, got) } - if d.widthMode != tc.wantWidthMode { - t.Errorf("widthMode: expected %v, got %v", tc.wantWidthMode, d.widthMode) + if got := d.WidthMode(); got != tc.wantWidthMode { + t.Errorf("WidthMode: expected %v, got %v", tc.wantWidthMode, got) } if got := float64(d.height); got != tc.wantHeight { t.Errorf("height: expected %v, got %v", tc.wantHeight, got) @@ -51,8 +53,8 @@ func TestDimensions_SetAttrs(t *testing.T) { if got := float64(d.heightValue); got != tc.wantHeightValue { t.Errorf("heightValue: expected %v, got %v", tc.wantHeightValue, got) } - if d.heightMode != tc.wantHeightMode { - t.Errorf("heightMode: expected %v, got %v", tc.wantHeightMode, d.heightMode) + if got := d.HeightMode(); got != tc.wantHeightMode { + t.Errorf("HeightMode: expected %v, got %v", tc.wantHeightMode, got) } if got := d.WidthIsSet(); got != tc.wantWidthSet { t.Errorf("WidthIsSet: expected %v, got %v", tc.wantWidthSet, got) @@ -64,6 +66,18 @@ func TestDimensions_SetAttrs(t *testing.T) { } } +func TestDimensions_SetAttrs_AutoIsCaseSensitive(t *testing.T) { + var d Dimensions + d.SetAttrs(map[string]string{"width": "AUTO", "height": "Auto"}, "") + + if got := d.WidthMode(); got == DimAuto { + t.Fatalf("WidthMode() = %v, want non-auto for uppercase AUTO", got) + } + if got := d.HeightMode(); got == DimAuto { + t.Fatalf("HeightMode() = %v, want non-auto for mixed-case Auto", got) + } +} + func TestStdWidget_DimensionResolution(t *testing.T) { page := &StdPage{pageStyle: &PageStyle{width: 200, height: 120}} @@ -88,6 +102,21 @@ func TestStdWidget_DimensionResolution(t *testing.T) { if got := rel.Height(); got != 135 { t.Fatalf("rel.Height() = %v, want 135", got) } + + auto := &StdWidget{} + _ = auto.SetContainer(page) + auto.SetWidthAuto() + auto.SetHeightAuto() + auto.SetLeft(10) + auto.SetRight(-10) + auto.SetTop(5) + auto.SetBottom(-5) + if got := auto.Width(); got != 180 { + t.Fatalf("auto.Width() = %v, want 180", got) + } + if got := auto.Height(); got != 110 { + t.Fatalf("auto.Height() = %v, want 110", got) + } } func TestDetectWidths_PreservesPercentClassification(t *testing.T) { @@ -119,6 +148,24 @@ func TestDetectWidths_PreservesPercentClassification(t *testing.T) { } } +func TestDetectWidths_TreatsAutoAsUnspecified(t *testing.T) { + page := &StdPage{pageStyle: &PageStyle{width: 200, height: 120}} + grid := NewWidgetGrid(1, 1) + + auto := &StdWidget{} + _ = auto.SetContainer(page) + auto.SetWidthAuto() + grid.SetCell(0, 0, auto) + + widths := detectWidths(grid, nil) + if got := widths[0].How; got != Unspecified { + t.Fatalf("widths[0].How = %v, want %v", got, Unspecified) + } + if got := widths[0].Size; got != 0 { + t.Fatalf("widths[0].Size = %v, want 0", got) + } +} + func TestStdIndex_ClearMeasuredGeometry_ClearsOnlyImplicitDimensions(t *testing.T) { index := &StdIndex{} index.width = 140 diff --git a/ltml/dir_test.go b/ltml/dir_test.go index 846b458..ad12667 100644 --- a/ltml/dir_test.go +++ b/ltml/dir_test.go @@ -195,26 +195,6 @@ func TestLayoutVBox_RTL(t *testing.T) { } } -func TestLayoutVBox_AlignSelfCenterCentersTopChildHorizontally(t *testing.T) { - c := positionedContainer(0, 0, 300, 200) - style := &LayoutStyle{} - - w := &positionedTestWidget{preferredWidth: 100, preferredHeight: 30} - w.SetWidth(100) - w.SetAttrs(map[string]string{"align": "top", "align-self": "center"}) - w.SetContainer(c) - c.AddChild(w) - - LayoutVBox(c, style, nil) - - if got := w.Left(); got != 100 { - t.Errorf("vbox top child left = %v, want 100", got) - } - if got := w.Top(); got != 0 { - t.Errorf("vbox top child top = %v, want 0", got) - } -} - func TestLayoutVBox_AlignSelfEndMirrorsInRTL(t *testing.T) { c := rtlContainer(0, 0, 300, 200) style := &LayoutStyle{} @@ -232,27 +212,6 @@ func TestLayoutVBox_AlignSelfEndMirrorsInRTL(t *testing.T) { } } -func TestLayoutVBox_ParagraphDefaultsToFullWidth(t *testing.T) { - c := positionedContainer(0, 0, 300, 200) - p := &StdParagraph{} - if err := p.SetContainer(c); err != nil { - t.Fatal(err) - } - p.paragraphStyle = &ParagraphStyle{} - p.font = &FontStyle{id: "body", entries: []fontEntry{{name: "Helvetica"}}, size: 12} - p.AddText("Short heading") - c.AddChild(p) - - LayoutVBox(c, &LayoutStyle{}, &labelTestWriter{t: t}) - - if got := p.Width(); got != 300 { - t.Fatalf("paragraph width = %v, want 300", got) - } - if got := p.Left(); got != 0 { - t.Fatalf("paragraph left = %v, want 0", got) - } -} - func TestLayoutHBox_RTL(t *testing.T) { c := rtlContainer(0, 0, 300, 100) style := &LayoutStyle{} @@ -278,60 +237,6 @@ func TestLayoutHBox_RTL(t *testing.T) { } } -func TestLayoutHBox_AlignSelfCenterCentersLeftChildVertically(t *testing.T) { - c := positionedContainer(0, 0, 300, 100) - style := &LayoutStyle{} - - w := &positionedTestWidget{preferredWidth: 80, preferredHeight: 20} - w.SetWidth(80) - w.SetAttrs(map[string]string{"align": "left", "align-self": "center"}) - w.SetContainer(c) - c.AddChild(w) - - LayoutHBox(c, style, nil) - - if got := w.Top(); got != 40 { - t.Errorf("hbox left child top = %v, want 40", got) - } - if got := w.Left(); got != 0 { - t.Errorf("hbox left child left = %v, want 0", got) - } -} - -func TestLayoutHBox_ContainerAlignBottomStillBottomAlignsChildrenByDefault(t *testing.T) { - c := positionedContainer(0, 0, 300, 100) - c.align = AlignBottom - style := &LayoutStyle{} - - w := &positionedTestWidget{preferredWidth: 80, preferredHeight: 20} - w.SetWidth(80) - w.SetContainer(c) - c.AddChild(w) - - LayoutHBox(c, style, nil) - - if got := w.Top(); got != 80 { - t.Errorf("default hbox child top with container align bottom = %v, want 80", got) - } -} - -func TestLayoutHBox_AlignSelfEndBottomAlignsChild(t *testing.T) { - c := positionedContainer(0, 0, 300, 100) - style := &LayoutStyle{} - - w := &positionedTestWidget{preferredWidth: 80, preferredHeight: 20} - w.SetWidth(80) - w.SetAttrs(map[string]string{"align-self": "end"}) - w.SetContainer(c) - c.AddChild(w) - - LayoutHBox(c, style, nil) - - if got := w.Top(); got != 80 { - t.Errorf("hbox end-aligned child top = %v, want 80", got) - } -} - func TestLayoutHBox_RTL_AlignedPanels(t *testing.T) { c := rtlContainer(0, 0, 300, 100) style := &LayoutStyle{} diff --git a/ltml/layout_box.go b/ltml/layout_box.go index bda5e85..fe031cd 100644 --- a/ltml/layout_box.go +++ b/ltml/layout_box.go @@ -22,14 +22,16 @@ func LayoutHBox(container Container, style *LayoutStyle, writer Writer) { } } - var percents, specified, others []Widget + var percents, specified, omitted, auto []Widget for _, widget := range static { if widget.WidthPctIsSet() { percents = append(percents, widget) + } else if widget.WidthMode() == DimAuto { + auto = append(auto, widget) } else if widget.WidthIsSet() { specified = append(specified, widget) } else { - others = append(others, widget) + omitted = append(omitted, widget) } } @@ -63,15 +65,47 @@ func LayoutHBox(container Container, style *LayoutStyle, writer Writer) { } widthAvail -= style.HPadding() - if widthAvail-float64(len(others)-1)*style.HPadding() >= float64(len(others)) { - widthAvail -= float64(len(others)-1) * style.HPadding() - othersWidth := widthAvail / float64(len(others)) - for _, widget := range others { - widget.SetWidth(othersWidth) + if len(auto) > 0 { + remaining := make([]Widget, 0, len(omitted)+len(auto)) + remaining = append(remaining, omitted...) + remaining = append(remaining, auto...) + paddingCost := float64(len(remaining)-1) * style.HPadding() + preferredTotal := 0.0 + for _, widget := range remaining { + preferredTotal += widget.PreferredWidth(writer) } - } else { + if widthAvail > preferredTotal+paddingCost { + widthAvail -= paddingCost + for _, widget := range omitted { + pw := widget.PreferredWidth(writer) + widget.SetWidth(pw) + widthAvail -= pw + } + autoWidth := widthAvail / float64(len(auto)) + for _, widget := range auto { + widget.SetWidth(autoWidth) + } + } else if widthAvail-float64(len(remaining)-1)*style.HPadding() >= float64(len(remaining)) { + widthAvail -= float64(len(remaining)-1) * style.HPadding() + remainingWidth := widthAvail / float64(len(remaining)) + for _, widget := range remaining { + widget.SetWidth(remainingWidth) + } + } else { + containerFull = true + for _, widget := range remaining { + widget.SetDisabled(true) + } + } + } else if len(omitted) > 0 && widthAvail-float64(len(omitted)-1)*style.HPadding() >= float64(len(omitted)) { + widthAvail -= float64(len(omitted)-1) * style.HPadding() + omittedWidth := widthAvail / float64(len(omitted)) + for _, widget := range omitted { + widget.SetWidth(omittedWidth) + } + } else if len(omitted) > 0 { containerFull = true - for _, widget := range others { + for _, widget := range omitted { widget.SetDisabled(true) } } diff --git a/ltml/layout_box_test.go b/ltml/layout_box_test.go new file mode 100644 index 0000000..0ab127b --- /dev/null +++ b/ltml/layout_box_test.go @@ -0,0 +1,272 @@ +package ltml + +import ( + "math" + "testing" +) + +func TestLayoutVBox_AlignSelfCenterCentersTopChildHorizontally(t *testing.T) { + c := positionedContainer(0, 0, 300, 200) + style := &LayoutStyle{} + + w := &positionedTestWidget{preferredWidth: 100, preferredHeight: 30} + w.SetWidth(100) + w.SetAttrs(map[string]string{"align": "top", "align-self": "center"}) + w.SetContainer(c) + c.AddChild(w) + + LayoutVBox(c, style, nil) + + if got := w.Left(); got != 100 { + t.Errorf("vbox top child left = %v, want 100", got) + } + if got := w.Top(); got != 0 { + t.Errorf("vbox top child top = %v, want 0", got) + } +} + +func TestLayoutVBox_ParagraphDefaultsToFullWidth(t *testing.T) { + c := positionedContainer(0, 0, 300, 200) + p := &StdParagraph{} + if err := p.SetContainer(c); err != nil { + t.Fatal(err) + } + p.paragraphStyle = &ParagraphStyle{} + p.font = &FontStyle{id: "body", entries: []fontEntry{{name: "Helvetica"}}, size: 12} + p.AddText("Short heading") + c.AddChild(p) + + LayoutVBox(c, &LayoutStyle{}, &labelTestWriter{t: t}) + + if got := p.Width(); got != 300 { + t.Fatalf("paragraph width = %v, want 300", got) + } + if got := p.Left(); got != 0 { + t.Fatalf("paragraph left = %v, want 0", got) + } +} + +func TestLayoutHBox_AutoWidthAbsorbsOnlySurplusSpace(t *testing.T) { + c := positionedContainer(0, 0, 400, 100) + style := &LayoutStyle{hpadding: 10} + + fixed := &positionedTestWidget{preferredWidth: 40, preferredHeight: 20} + fixed.SetWidth(40) + if err := fixed.SetContainer(c); err != nil { + t.Fatal(err) + } + c.AddChild(fixed) + + pct := &positionedTestWidget{preferredWidth: 30, preferredHeight: 20} + pct.SetWidthPct(25) + if err := pct.SetContainer(c); err != nil { + t.Fatal(err) + } + c.AddChild(pct) + + omitted := &positionedTestWidget{preferredWidth: 80, preferredHeight: 20} + if err := omitted.SetContainer(c); err != nil { + t.Fatal(err) + } + c.AddChild(omitted) + + auto1 := &positionedTestWidget{preferredWidth: 60, preferredHeight: 20} + auto1.SetWidthAuto() + if err := auto1.SetContainer(c); err != nil { + t.Fatal(err) + } + c.AddChild(auto1) + + auto2 := &positionedTestWidget{preferredWidth: 40, preferredHeight: 20} + auto2.SetWidthAuto() + if err := auto2.SetContainer(c); err != nil { + t.Fatal(err) + } + c.AddChild(auto2) + + LayoutHBox(c, style, nil) + + if got := fixed.Width(); got != 40 { + t.Fatalf("fixed.Width() = %v, want 40", got) + } + if got := pct.Width(); got != 100 { + t.Fatalf("pct.Width() = %v, want 100", got) + } + if got := omitted.Width(); got != 80 { + t.Fatalf("omitted.Width() = %v, want 80", got) + } + if got := auto1.Width(); got != 70 { + t.Fatalf("auto1.Width() = %v, want 70", got) + } + if got := auto2.Width(); got != 70 { + t.Fatalf("auto2.Width() = %v, want 70", got) + } +} + +func TestLayoutHBox_AutoWidthHasNoEffectWhenConstrained(t *testing.T) { + c := positionedContainer(0, 0, 340, 100) + style := &LayoutStyle{hpadding: 10} + + fixed := &positionedTestWidget{preferredWidth: 40, preferredHeight: 20} + fixed.SetWidth(40) + if err := fixed.SetContainer(c); err != nil { + t.Fatal(err) + } + c.AddChild(fixed) + + pct := &positionedTestWidget{preferredWidth: 30, preferredHeight: 20} + pct.SetWidthPct(25) + if err := pct.SetContainer(c); err != nil { + t.Fatal(err) + } + c.AddChild(pct) + + omitted := &positionedTestWidget{preferredWidth: 80, preferredHeight: 20} + if err := omitted.SetContainer(c); err != nil { + t.Fatal(err) + } + c.AddChild(omitted) + + auto1 := &positionedTestWidget{preferredWidth: 60, preferredHeight: 20} + auto1.SetWidthAuto() + if err := auto1.SetContainer(c); err != nil { + t.Fatal(err) + } + c.AddChild(auto1) + + auto2 := &positionedTestWidget{preferredWidth: 40, preferredHeight: 20} + auto2.SetWidthAuto() + if err := auto2.SetContainer(c); err != nil { + t.Fatal(err) + } + c.AddChild(auto2) + + LayoutHBox(c, style, nil) + + for _, widget := range []*positionedTestWidget{omitted, auto1, auto2} { + if got := widget.Width(); math.Abs(got-(175.0/3.0)) > 0.001 { + t.Fatalf("widget.Width() = %v, want %v", got, 175.0/3.0) + } + } +} + +func TestLayoutHBox_WithoutAutoKeepsExistingEqualShareBehavior(t *testing.T) { + c := positionedContainer(0, 0, 300, 100) + style := &LayoutStyle{hpadding: 10} + + fixed := &positionedTestWidget{preferredWidth: 40, preferredHeight: 20} + fixed.SetWidth(40) + if err := fixed.SetContainer(c); err != nil { + t.Fatal(err) + } + c.AddChild(fixed) + + pct := &positionedTestWidget{preferredWidth: 30, preferredHeight: 20} + pct.SetWidthPct(100.0 / 3.0) + if err := pct.SetContainer(c); err != nil { + t.Fatal(err) + } + c.AddChild(pct) + + omitted1 := &positionedTestWidget{preferredWidth: 80, preferredHeight: 20} + if err := omitted1.SetContainer(c); err != nil { + t.Fatal(err) + } + c.AddChild(omitted1) + + omitted2 := &positionedTestWidget{preferredWidth: 50, preferredHeight: 20} + if err := omitted2.SetContainer(c); err != nil { + t.Fatal(err) + } + c.AddChild(omitted2) + + LayoutHBox(c, style, nil) + + if got := omitted1.Width(); math.Abs(got-65) > 0.001 { + t.Fatalf("omitted1.Width() = %v, want 65", got) + } + if got := omitted2.Width(); math.Abs(got-65) > 0.001 { + t.Fatalf("omitted2.Width() = %v, want 65", got) + } +} + +func TestLayoutHBox_AlignSelfCenterCentersLeftChildVertically(t *testing.T) { + c := positionedContainer(0, 0, 300, 100) + style := &LayoutStyle{} + + w := &positionedTestWidget{preferredWidth: 80, preferredHeight: 20} + w.SetWidth(80) + w.SetAttrs(map[string]string{"align": "left", "align-self": "center"}) + w.SetContainer(c) + c.AddChild(w) + + LayoutHBox(c, style, nil) + + if got := w.Top(); got != 40 { + t.Errorf("hbox left child top = %v, want 40", got) + } + if got := w.Left(); got != 0 { + t.Errorf("hbox left child left = %v, want 0", got) + } +} + +func TestLayoutVBox_AutoWidthMatchesOmittedWidth(t *testing.T) { + c := positionedContainer(0, 0, 200, 100) + style := &LayoutStyle{} + + omitted := &positionedTestWidget{preferredWidth: 80, preferredHeight: 20} + if err := omitted.SetContainer(c); err != nil { + t.Fatal(err) + } + c.AddChild(omitted) + + auto := &positionedTestWidget{preferredWidth: 80, preferredHeight: 20} + auto.SetWidthAuto() + if err := auto.SetContainer(c); err != nil { + t.Fatal(err) + } + c.AddChild(auto) + + LayoutVBox(c, style, nil) + + if got := omitted.Width(); got != 80 { + t.Fatalf("omitted.Width() = %v, want 80", got) + } + if got := auto.Width(); got != 80 { + t.Fatalf("auto.Width() = %v, want 80", got) + } +} + +func TestLayoutHBox_ContainerAlignBottomStillBottomAlignsChildrenByDefault(t *testing.T) { + c := positionedContainer(0, 0, 300, 100) + c.align = AlignBottom + style := &LayoutStyle{} + + w := &positionedTestWidget{preferredWidth: 80, preferredHeight: 20} + w.SetWidth(80) + w.SetContainer(c) + c.AddChild(w) + + LayoutHBox(c, style, nil) + + if got := w.Top(); got != 80 { + t.Errorf("default hbox child top with container align bottom = %v, want 80", got) + } +} + +func TestLayoutHBox_AlignSelfEndBottomAlignsChild(t *testing.T) { + c := positionedContainer(0, 0, 300, 100) + style := &LayoutStyle{} + + w := &positionedTestWidget{preferredWidth: 80, preferredHeight: 20} + w.SetWidth(80) + w.SetAttrs(map[string]string{"align-self": "end"}) + w.SetContainer(c) + c.AddChild(w) + + LayoutHBox(c, style, nil) + + if got := w.Top(); got != 80 { + t.Errorf("hbox end-aligned child top = %v, want 80", got) + } +} diff --git a/ltml/ltml_test.go b/ltml/ltml_test.go index d63cc1c..de4547a 100644 --- a/ltml/ltml_test.go +++ b/ltml/ltml_test.go @@ -247,6 +247,7 @@ func TestSamples(t *testing.T) { "test_053_avery_labels", "test_054_canvas_draw", "test_055_arabic_index", + "test_056_hbox_auto_width", } for _, sample := range samples { diff --git a/ltml/samples/test_056_hbox_auto_width.ltml b/ltml/samples/test_056_hbox_auto_width.ltml new file mode 100644 index 0000000..5233e3c --- /dev/null +++ b/ltml/samples/test_056_hbox_auto_width.ltml @@ -0,0 +1,76 @@ + + + + + + + + + + + + +

+ When an hbox has surplus room, omitted widths keep their preferred size + and only width="auto" children absorb the slack. In a constrained hbox, + auto behaves the same as an omitted width. +

+ +
Roomy container
+
+ + + + +
+ +
Constrained container
+
+ + + + +
+
+
diff --git a/ltml/samples/test_056_hbox_auto_width.pdf b/ltml/samples/test_056_hbox_auto_width.pdf new file mode 100644 index 0000000000000000000000000000000000000000..5936fcf643f97ca4646955edb8dfa4d227cc4156 GIT binary patch literal 34944 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 Date: Mon, 4 May 2026 08:00:11 -0700 Subject: [PATCH 3/5] Add auto dimensions to LTML vbox layouts --- ltml/SYNTAX.md | 11 +- ltml/canvas_capture.go | 2 + ltml/dimensions.go | 61 +++- ltml/dimensions_test.go | 110 ++++++-- ltml/layout_absolute.go | 4 +- ltml/layout_box.go | 262 ++++++++++++++---- ltml/layout_box_test.go | 183 ++++++++++++ ltml/layout_flow.go | 23 +- ltml/layout_flow_test.go | 128 +++++++++ ltml/layout_overflow_test.go | 160 +++++++++++ ltml/layout_radial.go | 8 +- ltml/layout_table.go | 8 +- ltml/linking.go | 2 + ltml/ltml_test.go | 2 + ltml/radial_layout_test.go | 39 +++ ltml/samples/test_051_paper_sizes.pdf | Bin 151001 -> 151005 bytes ltml/samples/test_057_vbox_auto_height.ltml | 75 +++++ ltml/samples/test_057_vbox_auto_height.pdf | Bin 0 -> 35949 bytes .../test_058_vbox_auto_height_overflow.ltml | 55 ++++ .../test_058_vbox_auto_height_overflow.pdf | Bin 0 -> 33039 bytes ltml/std_container.go | 33 ++- ltml/std_index.go | 16 +- ltml/std_page.go | 19 +- ltml/std_paragraph.go | 3 +- ltml/std_widget.go | 18 +- ltml/widget.go | 4 + 26 files changed, 1084 insertions(+), 142 deletions(-) create mode 100644 ltml/layout_flow_test.go create mode 100644 ltml/samples/test_057_vbox_auto_height.ltml create mode 100644 ltml/samples/test_057_vbox_auto_height.pdf create mode 100644 ltml/samples/test_058_vbox_auto_height_overflow.ltml create mode 100644 ltml/samples/test_058_vbox_auto_height_overflow.pdf diff --git a/ltml/SYNTAX.md b/ltml/SYNTAX.md index 6c8d16d..28e97bf 100644 --- a/ltml/SYNTAX.md +++ b/ltml/SYNTAX.md @@ -1169,6 +1169,15 @@ not automatically change paragraph shaping or bidi behavior inside text widgets. - Use `align-self="start"`, `align-self="center"`, or `align-self="end"` to control horizontal placement within the vbox row. - In `dir="rtl"`, `align-self="start"` means right and `align-self="end"` means left. - In `dir="rtl"`, children are flush against the right edge plus padding instead of the left. +- When at least one child uses `height="auto"` and a height-constrained vbox + fragment has true surplus height beyond the specified, percent, and preferred + heights of the children on that fragment plus `layout.vpadding`, omitted + heights keep their preferred heights and the `auto` children split the + leftover height evenly. +- In natural-height vboxes, and in constrained vboxes without surplus, + `height="auto"` behaves the same as an omitted height. +- When a vbox splits across pages, `height="auto"` is evaluated separately for + each fragment page based only on the children present on that fragment. ### HBox Details @@ -1359,7 +1368,7 @@ Measurements can be expressed in several forms: | With unit | `1in`, `2.5cm`, `14pt` | Explicit unit overrides the current `units`. | | Percentage | `50%` | Percentage of the container's content width or height. | | Relative | `+10`, `-5` | Offset from the container's content dimension. | -| Auto | `auto` | Automatic layout-managed size. Currently special-cased only for `hbox` width; elsewhere it behaves like omitting the dimension. | +| Auto | `auto` | Automatic layout-managed size. Currently special-cased for `hbox` width and `vbox` height; elsewhere it behaves like omitting the dimension. | **Supported units:** `pt` (points), `in` (inches, 72pt), `cm` (centimeters, 28.35pt). diff --git a/ltml/canvas_capture.go b/ltml/canvas_capture.go index d97c25b..43b3d4d 100644 --- a/ltml/canvas_capture.go +++ b/ltml/canvas_capture.go @@ -51,6 +51,8 @@ func resetCanvasWidgetRenderState(root Widget) { widget.SetPrinted(false) widget.SetVisible(true) widget.SetDisabled(false) + widget.ClearResolvedWidth() + widget.ClearResolvedHeight() switch value := widget.(type) { case *StdContainer: value.activeChildren = nil diff --git a/ltml/dimensions.go b/ltml/dimensions.go index 6e859b9..93cb874 100644 --- a/ltml/dimensions.go +++ b/ltml/dimensions.go @@ -14,7 +14,7 @@ type DimensionMode int8 const ( DimUnspecified DimensionMode = iota - DimSpecified + DimLiteral DimPct DimRel DimAuto @@ -31,12 +31,15 @@ type Dimensions struct { heightValue float32 widthMode DimensionMode heightMode DimensionMode + widthValid bool + heightValid bool } type dimensionState struct { resolved float32 value float32 mode DimensionMode + valid bool } type dimensionsState struct { @@ -137,36 +140,58 @@ func (d *Dimensions) SetAttrs(attrs map[string]string, units Units) { func (d *Dimensions) SetHeight(value float64) { d.height = float32(value) d.heightValue = float32(value) - d.heightMode = DimSpecified + d.heightMode = DimLiteral + d.heightValid = true } func (d *Dimensions) SetHeightAuto() { d.height = 0 d.heightValue = 0 d.heightMode = DimAuto + d.heightValid = false } func (d *Dimensions) ClearHeight() { d.height = 0 d.heightValue = 0 d.heightMode = DimUnspecified + d.heightValid = false } func (d *Dimensions) SetHeightPct(value float64) { d.height = 0 d.heightValue = float32(value) d.heightMode = DimPct + d.heightValid = false } func (d *Dimensions) SetHeightRel(value float64) { d.height = 0 d.heightValue = float32(value) d.heightMode = DimRel + d.heightValid = false +} + +func (d *Dimensions) ResolveHeight(value float64) { + d.height = float32(value) + d.heightValid = true +} + +func (d *Dimensions) ClearResolvedHeight() { + if d.heightMode == DimLiteral { + d.height = d.heightValue + } else { + d.height = 0 + } + d.heightValid = false } func (d *Dimensions) HeightIsSet() bool { + if d.heightValid { + return true + } switch d.heightMode { - case DimSpecified, DimPct, DimRel: + case DimLiteral, DimPct, DimRel: return true default: return false @@ -192,31 +217,50 @@ func (d *Dimensions) SetLeft(value float64) { func (d *Dimensions) SetWidth(value float64) { d.width = float32(value) d.widthValue = float32(value) - d.widthMode = DimSpecified + d.widthMode = DimLiteral + d.widthValid = true } func (d *Dimensions) SetWidthAuto() { d.width = 0 d.widthValue = 0 d.widthMode = DimAuto + d.widthValid = false } func (d *Dimensions) ClearWidth() { d.width = 0 d.widthValue = 0 d.widthMode = DimUnspecified + d.widthValid = false } func (d *Dimensions) SetWidthPct(value float64) { d.width = 0 d.widthValue = float32(value) d.widthMode = DimPct + d.widthValid = false } func (d *Dimensions) SetWidthRel(value float64) { d.width = 0 d.widthValue = float32(value) d.widthMode = DimRel + d.widthValid = false +} + +func (d *Dimensions) ResolveWidth(value float64) { + d.width = float32(value) + d.widthValid = true +} + +func (d *Dimensions) ClearResolvedWidth() { + if d.widthMode == DimLiteral { + d.width = d.widthValue + } else { + d.width = 0 + } + d.widthValid = false } func (d *Dimensions) String() string { @@ -233,8 +277,11 @@ func (d *Dimensions) WidthRelIsSet() bool { } func (d *Dimensions) WidthIsSet() bool { + if d.widthValid { + return true + } switch d.widthMode { - case DimSpecified, DimPct, DimRel: + case DimLiteral, DimPct, DimRel: return true default: return false @@ -255,11 +302,13 @@ func (d *Dimensions) SaveState() dimensionsState { resolved: d.width, value: d.widthValue, mode: d.widthMode, + valid: d.widthValid, }, height: dimensionState{ resolved: d.height, value: d.heightValue, mode: d.heightMode, + valid: d.heightValid, }, } } @@ -268,7 +317,9 @@ func (d *Dimensions) RestoreState(state dimensionsState) { d.width = state.width.resolved d.widthValue = state.width.value d.widthMode = state.width.mode + d.widthValid = state.width.valid d.height = state.height.resolved d.heightValue = state.height.value d.heightMode = state.height.mode + d.heightValid = state.height.valid } diff --git a/ltml/dimensions_test.go b/ltml/dimensions_test.go index 2e685ed..4b99694 100644 --- a/ltml/dimensions_test.go +++ b/ltml/dimensions_test.go @@ -20,12 +20,12 @@ func TestDimensions_SetAttrs(t *testing.T) { wantWidthSet bool wantHeightSet bool }{ - {name: "Width", attrs: map[string]string{"width": "30"}, wantWidth: 30, wantWidthValue: 30, wantWidthMode: DimSpecified, wantWidthSet: true}, + {name: "Width", attrs: map[string]string{"width": "30"}, wantWidth: 30, wantWidthValue: 30, wantWidthMode: DimLiteral, wantWidthSet: true}, {name: "WidthPct", attrs: map[string]string{"width": "40%"}, wantWidthValue: 40, wantWidthMode: DimPct, wantWidthSet: true}, {name: "WidthRelPlus", attrs: map[string]string{"width": "+50"}, wantWidthValue: 50, wantWidthMode: DimRel, wantWidthSet: true}, {name: "WidthRelMinus", attrs: map[string]string{"width": "-60"}, wantWidthValue: -60, wantWidthMode: DimRel, wantWidthSet: true}, {name: "WidthAuto", attrs: map[string]string{"width": "auto"}, wantWidthMode: DimAuto}, - {name: "Height", attrs: map[string]string{"height": "30"}, wantHeight: 30, wantHeightValue: 30, wantHeightMode: DimSpecified, wantHeightSet: true}, + {name: "Height", attrs: map[string]string{"height": "30"}, wantHeight: 30, wantHeightValue: 30, wantHeightMode: DimLiteral, wantHeightSet: true}, {name: "HeightPct", attrs: map[string]string{"height": "40%"}, wantHeightValue: 40, wantHeightMode: DimPct, wantHeightSet: true}, {name: "HeightRelPlus", attrs: map[string]string{"height": "+50"}, wantHeightValue: 50, wantHeightMode: DimRel, wantHeightSet: true}, {name: "HeightRelMinus", attrs: map[string]string{"height": "-60"}, wantHeightValue: -60, wantHeightMode: DimRel, wantHeightSet: true}, @@ -168,31 +168,26 @@ func TestDetectWidths_TreatsAutoAsUnspecified(t *testing.T) { func TestStdIndex_ClearMeasuredGeometry_ClearsOnlyImplicitDimensions(t *testing.T) { index := &StdIndex{} - index.width = 140 - index.widthValue = 25 - index.widthMode = DimPct - index.height = 90 - index.heightValue = 12 - index.heightMode = DimRel + index.ResolveWidth(140) + index.ResolveHeight(90) index.clearMeasuredGeometry() - if index.width != 0 || index.widthValue != 0 || index.widthMode != DimUnspecified { - t.Fatalf("implicit width not cleared: width=%v value=%v mode=%v", index.width, index.widthValue, index.widthMode) + if index.width != 0 || index.widthValue != 0 || index.widthMode != DimUnspecified || index.widthValid { + t.Fatalf("implicit width not cleared: width=%v value=%v mode=%v valid=%v", index.width, index.widthValue, index.widthMode, index.widthValid) } - if index.height != 0 || index.heightValue != 0 || index.heightMode != DimUnspecified { - t.Fatalf("implicit height not cleared: height=%v value=%v mode=%v", index.height, index.heightValue, index.heightMode) + if index.height != 0 || index.heightValue != 0 || index.heightMode != DimUnspecified || index.heightValid { + t.Fatalf("implicit height not cleared: height=%v value=%v mode=%v valid=%v", index.height, index.heightValue, index.heightMode, index.heightValid) } - index.SetAttrs(map[string]string{"width": "40%", "height": "30"}) - index.width = 160 - index.height = 30 + index.SetAttrs(map[string]string{"width": "40%", "height": "30", "units": "pt"}) + index.ResolveWidth(160) index.clearMeasuredGeometry() - if index.width != 160 || index.widthValue != 40 || index.widthMode != DimPct { - t.Fatalf("explicit width was not preserved: width=%v value=%v mode=%v", index.width, index.widthValue, index.widthMode) + if index.width != 0 || index.widthValue != 40 || index.widthMode != DimPct || index.widthValid { + t.Fatalf("explicit width was not preserved: width=%v value=%v mode=%v valid=%v", index.width, index.widthValue, index.widthMode, index.widthValid) } - if index.height != 30 || index.heightValue != 30 || index.heightMode != DimSpecified { - t.Fatalf("explicit height was not preserved: height=%v value=%v mode=%v", index.height, index.heightValue, index.heightMode) + if index.height != 30 || index.heightValue != 30 || index.heightMode != DimLiteral || index.heightValid { + t.Fatalf("explicit height was not preserved: height=%v value=%v mode=%v valid=%v", index.height, index.heightValue, index.heightMode, index.heightValid) } } @@ -214,7 +209,82 @@ func TestDimensions_SaveStateAndClearHelpers(t *testing.T) { if d.widthMode != DimPct || d.widthValue != 40 || d.width != 0 { t.Fatalf("width state restore failed: mode=%v value=%v width=%v", d.widthMode, d.widthValue, d.width) } - if d.heightMode != DimSpecified || d.heightValue != 24 || d.height != 24 { + if d.heightMode != DimLiteral || d.heightValue != 24 || d.height != 24 { t.Fatalf("height state restore failed: mode=%v value=%v height=%v", d.heightMode, d.heightValue, d.height) } } + +func TestStdWidget_ResolveWidthPreservesSpecifiedModeAndOverridesUntilCleared(t *testing.T) { + page := &StdPage{pageStyle: &PageStyle{width: 200, height: 120}} + widget := &StdWidget{} + _ = widget.SetContainer(page) + widget.SetWidthPct(25) + + if got := widget.Width(); got != 50 { + t.Fatalf("Width() before resolve = %v, want 50", got) + } + + widget.ResolveWidth(80) + page.pageStyle.width = 400 + if got := widget.Width(); got != 80 { + t.Fatalf("Width() after resolve = %v, want 80", got) + } + if got := widget.WidthMode(); got != DimPct { + t.Fatalf("WidthMode() after resolve = %v, want %v", got, DimPct) + } + if got := widget.widthValue; got != 25 { + t.Fatalf("widthValue after resolve = %v, want 25", got) + } + + widget.ClearResolvedWidth() + if got := widget.Width(); got != 100 { + t.Fatalf("Width() after clear = %v, want 100", got) + } +} + +func TestStdWidget_ResolveAutoWidthOverridesSideResolutionUntilCleared(t *testing.T) { + page := &StdPage{pageStyle: &PageStyle{width: 200, height: 120}} + widget := &StdWidget{} + _ = widget.SetContainer(page) + widget.SetWidthAuto() + widget.SetLeft(10) + widget.SetRight(-10) + + if got := widget.Width(); got != 180 { + t.Fatalf("Width() before resolve = %v, want 180", got) + } + + widget.ResolveWidth(90) + widget.SetRight(-40) + if got := widget.Width(); got != 90 { + t.Fatalf("Width() after resolve = %v, want 90", got) + } + if got := widget.WidthIsSet(); !got { + t.Fatalf("WidthIsSet() after resolve = %v, want true", got) + } + + widget.ClearResolvedWidth() + if got := widget.Width(); got != 150 { + t.Fatalf("Width() after clear = %v, want 150", got) + } + if got := widget.WidthIsSet(); !got { + t.Fatalf("WidthIsSet() after clear = %v, want true from left/right anchors", got) + } +} + +func TestDimensions_ClearResolvedHeightPreservesSpecifiedHeight(t *testing.T) { + var d Dimensions + d.SetHeight(24) + d.ResolveHeight(36) + d.ClearResolvedHeight() + + if got := d.HeightMode(); got != DimLiteral { + t.Fatalf("HeightMode() = %v, want %v", got, DimLiteral) + } + if got := d.height; got != 24 { + t.Fatalf("height = %v, want 24", got) + } + if got := d.HeightIsSet(); !got { + t.Fatalf("HeightIsSet() = %v, want true", got) + } +} diff --git a/ltml/layout_absolute.go b/ltml/layout_absolute.go index 33bc942..e0c7fc5 100644 --- a/ltml/layout_absolute.go +++ b/ltml/layout_absolute.go @@ -39,11 +39,11 @@ func layoutWidgetsWithPosition(writer Writer, widgets []Widget, position Positio widget.SetTop(0) } if !widget.WidthIsSet() { - widget.SetWidth(widget.PreferredWidth(writer)) + widget.ResolveWidth(widget.PreferredWidth(writer)) } widget.LayoutWidget(writer) if !widget.HeightIsSet() { - widget.SetHeight(widget.PreferredHeight(writer)) + widget.ResolveHeight(widget.PreferredHeight(writer)) } } } diff --git a/ltml/layout_box.go b/ltml/layout_box.go index fe031cd..f486170 100644 --- a/ltml/layout_box.go +++ b/ltml/layout_box.go @@ -2,14 +2,37 @@ package ltml const layoutFitEpsilon = 0.001 +// LayoutHBox performs a single-pass horizontal box layout with a few important +// twists: +// +// - child widths are resolved in priority order so fixed commitments consume +// space before flexible ones +// - percent widths are scaled down proportionally when they collectively ask +// for more than the remaining width +// - omitted widths and width="auto" are similar in tight layouts, but in a +// roomy hbox only auto children absorb surplus while omitted children stay +// at preferred width +// - left/right-aligned panels are placed at the edges first, then the +// remaining unaligned children fill the center run +// +// The algorithm deliberately separates "resolve size" from "place children". +// Later layout managers and split logic depend on being able to understand +// exactly when a child became fixed to a concrete width or height. func LayoutHBox(container Container, style *LayoutStyle, writer Writer) { containerFull := false + // Static children participate in the box algorithm. Everything else is + // handled later by layoutPositionedChildren and should be hidden here so the + // static run is easy to reason about. static, remaining := printableWidgets(container, Static) for _, widget := range remaining { widget.SetVisible(false) } + // Alignment affects placement order, not sizing. We first split the static + // widgets into the three horizontal runs the hbox knows how to place: + // edge-pinned left panels, edge-pinned right panels, and the ordinary + // unaligned run between them. var lpanels, rpanels, unaligned []Widget for _, widget := range static { switch widget.Align() { @@ -22,21 +45,40 @@ func LayoutHBox(container Container, style *LayoutStyle, writer Writer) { } } + // Width modes drive the allocation algorithm. We intentionally classify + // children by "how width was authored" rather than by current resolved width, + // because prior probe/layout passes may already have stamped temporary + // resolved geometry onto the widget. + // + // The allocation priority is: + // 1. explicitly specified widths, including relative widths and widths + // implied by opposing horizontal sides + // 2. percent widths + // 3. omitted widths and auto widths + // + // The last two are separated because width="auto" has special roomy-layout + // behavior in hbox. var percents, specified, omitted, auto []Widget for _, widget := range static { if widget.WidthPctIsSet() { percents = append(percents, widget) - } else if widget.WidthMode() == DimAuto { + } else if widgetAutoWidth(widget) { auto = append(auto, widget) - } else if widget.WidthIsSet() { + } else if widgetWidthSpecified(widget) { specified = append(specified, widget) } else { omitted = append(omitted, widget) } } + // widthAvail tracks the content width that remains for the still-unresolved + // width groups. We subtract both child widths and the inter-child padding + // that must exist between committed children. widthAvail := ContentWidth(container) + // Specified widths are hard commitments: they consume space first and are not + // renegotiated by hbox. If they alone overfill the row, later widgets are + // disabled so they do not participate in placement or rendering. for _, widget := range specified { widthAvail -= widget.Width() containerFull = widthAvail < 0 @@ -44,6 +86,10 @@ func LayoutHBox(container Container, style *LayoutStyle, writer Writer) { widthAvail -= style.HPadding() } + // Percent widths are resolved against whatever width remains after the hard + // commitments above. If the requested percents over-allocate, they are scaled + // down proportionally instead of being treated as independent hard failures. + // If even the padding gaps cannot fit, the whole percent group is disabled. if widthAvail-float64(len(percents)-1)*style.HPadding() >= float64(len(percents)) { widthAvail -= float64(len(percents)-1) * style.HPadding() totalPercents := 0.0 @@ -53,7 +99,7 @@ func LayoutHBox(container Container, style *LayoutStyle, writer Writer) { ratio := widthAvail / totalPercents for _, widget := range percents { if ratio < 1.0 { - widget.SetWidth(widget.Width() * ratio) + widget.ResolveWidth(widget.Width() * ratio) } widthAvail -= widget.Width() } @@ -65,6 +111,13 @@ func LayoutHBox(container Container, style *LayoutStyle, writer Writer) { } widthAvail -= style.HPadding() + // The final allocation step handles children whose width was omitted or set + // to auto. + // + // In a traditional hbox, omitted widths simply split the leftover width + // evenly. The new auto mode only changes the roomy case: when there is more + // than enough space for everyone's preferred widths, omitted children keep + // their preferred width and only auto children absorb the slack. if len(auto) > 0 { remaining := make([]Widget, 0, len(omitted)+len(auto)) remaining = append(remaining, omitted...) @@ -78,18 +131,18 @@ func LayoutHBox(container Container, style *LayoutStyle, writer Writer) { widthAvail -= paddingCost for _, widget := range omitted { pw := widget.PreferredWidth(writer) - widget.SetWidth(pw) + widget.ResolveWidth(pw) widthAvail -= pw } autoWidth := widthAvail / float64(len(auto)) for _, widget := range auto { - widget.SetWidth(autoWidth) + widget.ResolveWidth(autoWidth) } } else if widthAvail-float64(len(remaining)-1)*style.HPadding() >= float64(len(remaining)) { widthAvail -= float64(len(remaining)-1) * style.HPadding() remainingWidth := widthAvail / float64(len(remaining)) for _, widget := range remaining { - widget.SetWidth(remainingWidth) + widget.ResolveWidth(remainingWidth) } } else { containerFull = true @@ -97,11 +150,13 @@ func LayoutHBox(container Container, style *LayoutStyle, writer Writer) { widget.SetDisabled(true) } } + // With no auto widths present, omitted children keep the historical hbox + // behavior: split whatever width remains evenly. } else if len(omitted) > 0 && widthAvail-float64(len(omitted)-1)*style.HPadding() >= float64(len(omitted)) { widthAvail -= float64(len(omitted)-1) * style.HPadding() omittedWidth := widthAvail / float64(len(omitted)) for _, widget := range omitted { - widget.SetWidth(omittedWidth) + widget.ResolveWidth(omittedWidth) } } else if len(omitted) > 0 { containerFull = true @@ -110,16 +165,25 @@ func LayoutHBox(container Container, style *LayoutStyle, writer Writer) { } } + // HBox does not negotiate heights. Unspecified or auto heights simply fall + // back to preferred height, then each child is vertically aligned within the + // container's cross axis. for _, widget := range static { - if !widget.HeightIsSet() { - widget.SetHeight(widget.PreferredHeight(writer)) + if widgetAutoHeight(widget) || !widgetHeightSpecified(widget) { + widget.ResolveHeight(widget.PreferredHeight(writer)) } widget.SetTop(hboxCrossAxisTop(container, widget)) } + // Placement is performed after every width is known. We maintain two moving + // edges so left/right-aligned panels can claim the outer slots first and the + // unaligned run naturally fills the space between them. left := ContentLeft(container) right := ContentRight(container) + // RTL reverses the interpretation of the logical left/right runs while + // keeping the same sizing decisions. The groups are still processed in a + // stable order so authored child order remains meaningful. if IsRTL(container) { for _, widget := range lpanels { if widget.Disabled() { @@ -168,6 +232,9 @@ func LayoutHBox(container Container, style *LayoutStyle, writer Writer) { } } + // An unsized hbox takes the tallest participating child as its content + // height. Child layout runs after this so nested widgets see the final box + // dimensions chosen above. if !container.HeightIsSet() { contentHeight := 0.0 for _, widget := range static { @@ -175,7 +242,7 @@ func LayoutHBox(container Container, style *LayoutStyle, writer Writer) { contentHeight = widget.Height() } } - container.SetHeight(contentHeight + NonContentHeight(container)) + container.ResolveHeight(contentHeight + NonContentHeight(container)) } for _, widget := range static { if widget.Visible() && !widget.Disabled() { @@ -185,12 +252,33 @@ func LayoutHBox(container Container, style *LayoutStyle, writer Writer) { layoutPositionedChildren(container, writer) } +// LayoutVBox performs vertical stacking with separate treatment for headers, +// body children, and footers. +// +// The vertical axis is more subtle than hbox because vbox must reconcile three +// concerns at once: +// +// - natural-height containers need to discover their own height from children +// - constrained containers must stop or split when they run out of room +// - height="auto" can absorb only true surplus height, and only after the +// baseline stack of specified, percent, and preferred heights has been +// computed +// +// As in hbox, the function first resolves the child sizes it needs, then +// performs placement with explicit overflow checks. func LayoutVBox(container Container, style *LayoutStyle, writer Writer) { containerFull := false + + // Only static children are part of the vertical flow. Positioned children are + // laid out afterward in their own coordinate system. static, remaining := printableWidgets(container, Static) for _, widget := range remaining { widget.SetVisible(false) } + + // Top-aligned children are treated as headers, bottom-aligned children as + // footers, and everything else as the body run. This lets vbox reserve footer + // space from the bottom while still laying out the main body top-to-bottom. var headers, footers, unaligned []Widget for _, widget := range static { switch widget.Align() { @@ -203,8 +291,14 @@ func LayoutVBox(container Container, style *LayoutStyle, writer Writer) { } } rtl := IsRTL(container) + + // Width resolution in vbox is simpler than hbox: children are stacked, so + // each child can independently take its preferred width up to the content + // width. Paragraphs are a deliberate special case because their natural width + // is effectively the full available measure rather than the unwrapped line + // width of their text. for _, widget := range static { - if !widget.WidthIsSet() { + if widgetAutoWidth(widget) || !widgetWidthSpecified(widget) { cw := ContentWidth(container) pw := 0.0 if _, ok := widget.(*StdParagraph); ok { @@ -216,41 +310,90 @@ func LayoutVBox(container Container, style *LayoutStyle, writer Writer) { pw = cw } w := min(pw, cw) - widget.SetWidth(w) + widget.ResolveWidth(w) } widget.SetLeft(vboxCrossAxisLeft(container, widget, rtl)) } + + // Before we place any child, compute the baseline vertical stack that this + // fragment would occupy with no surplus distribution. We keep the per-widget + // resolved height in a side map so we can later add auto-height surplus + // without losing track of the original preferred/specifed result. + resolvedHeights := make(map[Widget]float64, len(static)) + autoWidgets := make([]Widget, 0, len(static)) + baselineHeight := 0.0 + seen := 0 + for _, group := range [][]Widget{headers, unaligned, footers} { + for _, widget := range group { + height := widget.Height() + if !widgetHeightSpecified(widget) { + height = widget.PreferredHeight(writer) + if widgetAutoHeight(widget) && !widgetZeroFootprint(widget) { + autoWidgets = append(autoWidgets, widget) + } + } + resolvedHeights[widget] = height + if widgetZeroFootprint(widget) { + continue + } + if seen > 0 { + baselineHeight += style.VPadding() + } + baselineHeight += height + seen++ + } + } + + // For a natural-height vbox, the baseline stack determines the container's + // own resolved height. + // + // For a constrained vbox, the baseline is instead used to decide whether + // there is true surplus height that auto children should absorb. Omitted + // heights never absorb this slack; they stay at preferred height. if !container.HeightIsSet() { - container.SetHeight(vboxNaturalContentHeight(style, writer, headers, unaligned, footers) + NonContentHeight(container)) + container.ResolveHeight(baselineHeight + NonContentHeight(container)) + } else if len(autoWidgets) > 0 { + if surplus := ContentHeight(container) - baselineHeight; surplus > 0 { + extra := surplus / float64(len(autoWidgets)) + for _, widget := range autoWidgets { + resolvedHeights[widget] += extra + } + } + } + + // Commit the resolved heights for any child that was not already height- + // specified by the author. This keeps the later placement and nested layout + // passes working from a stable page-local height. + for _, widget := range static { + if !widgetHeightSpecified(widget) { + widget.ResolveHeight(resolvedHeights[widget]) + } } - top, dy := ContentTop(container), 0.0 + + // top advances downward through the content band. bottom is the maximum + // usable bottom edge for this fragment, which matters both for overflow and + // for footer placement. + top := ContentTop(container) bottom := ContentTop(container) + MaxContentHeight(container) - for i, widget := range headers { + // Headers consume space from the top in source order. + for _, widget := range headers { widget.SetTop(top) widget.LayoutWidget(writer) - if !widget.HeightIsSet() { - widget.SetHeight(widget.PreferredHeight(writer)) - } if widgetZeroFootprint(widget) { widget.SetVisible(widget.Top() <= bottom) continue } top += widget.Height() + style.VPadding() - dy += widget.Height() - if i > 0 { - dy += style.VPadding() - } widget.SetVisible(widget.Bottom() <= bottom) } + // Footers are placed from the bottom upward so they reserve their space + // before the body run is checked for overflow. if len(footers) > 0 { footerBottom := bottom for i := len(footers) - 1; i >= 0; i-- { widget := footers[i] - if !widget.HeightIsSet() { - widget.SetHeight(widget.PreferredHeight(writer)) - } widget.SetBottom(footerBottom) widget.LayoutWidget(writer) if widgetZeroFootprint(widget) { @@ -262,54 +405,31 @@ func LayoutVBox(container Container, style *LayoutStyle, writer Writer) { } } - for i, widget := range unaligned { + // The unaligned body run consumes whatever vertical band remains between the + // headers and footers. Once a non-zero-footprint child would cross the bottom + // edge, that child is hidden and later siblings are skipped for this fragment. + for _, widget := range unaligned { widget.SetVisible(!containerFull) if containerFull { continue } widget.SetTop(top) widget.LayoutWidget(writer) - if !widget.HeightIsSet() { - widget.SetHeight(widget.PreferredHeight(writer)) - } if widgetZeroFootprint(widget) { widget.SetVisible(widget.Top() <= bottom) continue } top += widget.Height() - dy += widget.Height() - if i > 0 { - dy += style.VPadding() - } if top > bottom+layoutFitEpsilon { containerFull = true widget.SetVisible(false) } top += style.VPadding() } - layoutPositionedChildren(container, writer) -} -func vboxNaturalContentHeight(style *LayoutStyle, writer Writer, groups ...[]Widget) float64 { - height := 0.0 - seen := 0 - for _, group := range groups { - for _, widget := range group { - if widgetZeroFootprint(widget) { - continue - } - if seen > 0 { - height += style.VPadding() - } - widgetHeight := widget.Height() - if !widget.HeightIsSet() { - widgetHeight = widget.PreferredHeight(writer) - } - height += widgetHeight - seen++ - } - } - return height + // Positioned children are intentionally outside the static stacking + // algorithm, so they are laid out after the vbox flow has settled. + layoutPositionedChildren(container, writer) } func hboxCrossAxisTop(container Container, widget Widget) float64 { @@ -326,6 +446,38 @@ func hboxCrossAxisTop(container Container, widget Widget) float64 { } } +func widgetWidthSpecified(widget Widget) bool { + if widget.LeftIsSet() && widget.RightIsSet() { + return true + } + switch widget.WidthMode() { + case DimLiteral, DimPct, DimRel: + return true + default: + return false + } +} + +func widgetHeightSpecified(widget Widget) bool { + if widget.TopIsSet() && widget.BottomIsSet() { + return true + } + switch widget.HeightMode() { + case DimLiteral, DimPct, DimRel: + return true + default: + return false + } +} + +func widgetAutoWidth(widget Widget) bool { + return widget.WidthMode() == DimAuto && !(widget.LeftIsSet() && widget.RightIsSet()) +} + +func widgetAutoHeight(widget Widget) bool { + return widget.HeightMode() == DimAuto && !(widget.TopIsSet() && widget.BottomIsSet()) +} + func vboxCrossAxisLeft(container Container, widget Widget, rtl bool) float64 { switch widget.SelfAlign() { case SelfAlignCenter: diff --git a/ltml/layout_box_test.go b/ltml/layout_box_test.go index 0ab127b..6aeaf3b 100644 --- a/ltml/layout_box_test.go +++ b/ltml/layout_box_test.go @@ -5,6 +5,15 @@ import ( "testing" ) +func testHBoxLabel(t *testing.T, text string) *StdLabel { + t.Helper() + label := &StdLabel{} + label.font = &FontStyle{id: "body", entries: []fontEntry{{name: "Helvetica"}}, size: 11} + label.SetAttrs(map[string]string{"padding": "6pt"}) + label.AddText(text) + return label +} + func TestLayoutVBox_AlignSelfCenterCentersTopChildHorizontally(t *testing.T) { c := positionedContainer(0, 0, 300, 200) style := &LayoutStyle{} @@ -150,6 +159,70 @@ func TestLayoutHBox_AutoWidthHasNoEffectWhenConstrained(t *testing.T) { } } +func TestLayoutHBox_AutoWidthMatchesDirectLayoutAfterVBoxProbe(t *testing.T) { + writer := &labelTestWriter{t: t} + hboxStyle := &LayoutStyle{manager: "hbox", hpadding: 10} + + buildBox := func() (*StdContainer, []*StdLabel) { + box := &StdContainer{} + box.layout = hboxStyle + box.SetWidth(3.9 * 72) + box.SetAttrs(map[string]string{"padding": "8pt"}) + + fixed := testHBoxLabel(t, "fixed") + fixed.SetWidth(0.75 * 72) + if err := fixed.SetContainer(box); err != nil { + t.Fatal(err) + } + box.AddChild(fixed) + + preferred := testHBoxLabel(t, "preferred width") + if err := preferred.SetContainer(box); err != nil { + t.Fatal(err) + } + box.AddChild(preferred) + + auto1 := testHBoxLabel(t, "auto A") + auto1.SetWidthAuto() + if err := auto1.SetContainer(box); err != nil { + t.Fatal(err) + } + box.AddChild(auto1) + + auto2 := testHBoxLabel(t, "auto B") + auto2.SetWidthAuto() + if err := auto2.SetContainer(box); err != nil { + t.Fatal(err) + } + box.AddChild(auto2) + + return box, []*StdLabel{fixed, preferred, auto1, auto2} + } + + directBox, directLabels := buildBox() + LayoutHBox(directBox, hboxStyle, writer) + directWidths := make([]float64, len(directLabels)) + for i, label := range directLabels { + directWidths[i] = label.Width() + } + + probedBox, probedLabels := buildBox() + outer := positionedContainer(0, 0, 400, 300) + outer.layout = &LayoutStyle{manager: "vbox"} + if err := probedBox.SetContainer(outer); err != nil { + t.Fatal(err) + } + outer.AddChild(probedBox) + + LayoutVBox(outer, outer.layout, writer) + + for i, label := range probedLabels { + if got := label.Width(); math.Abs(got-directWidths[i]) > 0.001 { + t.Fatalf("label %d width after vbox probe = %v, want %v", i, got, directWidths[i]) + } + } +} + func TestLayoutHBox_WithoutAutoKeepsExistingEqualShareBehavior(t *testing.T) { c := positionedContainer(0, 0, 300, 100) style := &LayoutStyle{hpadding: 10} @@ -237,6 +310,116 @@ func TestLayoutVBox_AutoWidthMatchesOmittedWidth(t *testing.T) { } } +func TestLayoutVBox_AutoHeightAbsorbsOnlySurplusSpace(t *testing.T) { + c := positionedContainer(0, 0, 200, 200) + style := &LayoutStyle{vpadding: 10} + + fixed := &positionedTestWidget{preferredWidth: 80, preferredHeight: 40} + fixed.SetHeight(40) + _ = fixed.SetContainer(c) + c.AddChild(fixed) + + pct := &positionedTestWidget{preferredWidth: 80, preferredHeight: 15} + pct.SetHeightPct(25) + _ = pct.SetContainer(c) + c.AddChild(pct) + + omitted := &positionedTestWidget{preferredWidth: 80, preferredHeight: 30} + _ = omitted.SetContainer(c) + c.AddChild(omitted) + + auto1 := &positionedTestWidget{preferredWidth: 80, preferredHeight: 20} + auto1.SetHeightAuto() + _ = auto1.SetContainer(c) + c.AddChild(auto1) + + auto2 := &positionedTestWidget{preferredWidth: 80, preferredHeight: 10} + auto2.SetHeightAuto() + _ = auto2.SetContainer(c) + c.AddChild(auto2) + + LayoutVBox(c, style, nil) + + if got := fixed.Height(); got != 40 { + t.Fatalf("fixed.Height() = %v, want 40", got) + } + if got := pct.Height(); got != 50 { + t.Fatalf("pct.Height() = %v, want 50", got) + } + if got := omitted.Height(); got != 30 { + t.Fatalf("omitted.Height() = %v, want 30", got) + } + if got := auto1.Height(); got != 25 { + t.Fatalf("auto1.Height() = %v, want 25", got) + } + if got := auto2.Height(); got != 15 { + t.Fatalf("auto2.Height() = %v, want 15", got) + } +} + +func TestLayoutVBox_AutoHeightHasNoEffectWhenNotRoomy(t *testing.T) { + c := positionedContainer(0, 0, 200, 130) + style := &LayoutStyle{vpadding: 10} + + fixed := &positionedTestWidget{preferredWidth: 80, preferredHeight: 40} + fixed.SetHeight(40) + _ = fixed.SetContainer(c) + c.AddChild(fixed) + + omitted := &positionedTestWidget{preferredWidth: 80, preferredHeight: 30} + _ = omitted.SetContainer(c) + c.AddChild(omitted) + + auto1 := &positionedTestWidget{preferredWidth: 80, preferredHeight: 20} + auto1.SetHeightAuto() + _ = auto1.SetContainer(c) + c.AddChild(auto1) + + auto2 := &positionedTestWidget{preferredWidth: 80, preferredHeight: 10} + auto2.SetHeightAuto() + _ = auto2.SetContainer(c) + c.AddChild(auto2) + + LayoutVBox(c, style, nil) + + if got := omitted.Height(); got != 30 { + t.Fatalf("omitted.Height() = %v, want 30", got) + } + if got := auto1.Height(); got != 20 { + t.Fatalf("auto1.Height() = %v, want 20", got) + } + if got := auto2.Height(); got != 10 { + t.Fatalf("auto2.Height() = %v, want 10", got) + } +} + +func TestLayoutVBox_AutoHeightMatchesOmittedHeightWhenContainerIsNatural(t *testing.T) { + c := positionedContainer(0, 0, 200, 0) + c.ClearHeight() + style := &LayoutStyle{vpadding: 10} + + omitted := &positionedTestWidget{preferredWidth: 80, preferredHeight: 30} + _ = omitted.SetContainer(c) + c.AddChild(omitted) + + auto := &positionedTestWidget{preferredWidth: 80, preferredHeight: 20} + auto.SetHeightAuto() + _ = auto.SetContainer(c) + c.AddChild(auto) + + LayoutVBox(c, style, nil) + + if got := omitted.Height(); got != 30 { + t.Fatalf("omitted.Height() = %v, want 30", got) + } + if got := auto.Height(); got != 20 { + t.Fatalf("auto.Height() = %v, want 20", got) + } + if got := c.Height(); got != 60 { + t.Fatalf("container.Height() = %v, want 60", got) + } +} + func TestLayoutHBox_ContainerAlignBottomStillBottomAlignsChildrenByDefault(t *testing.T) { c := positionedContainer(0, 0, 300, 100) c.align = AlignBottom diff --git a/ltml/layout_flow.go b/ltml/layout_flow.go index a8e8228..9c65039 100644 --- a/ltml/layout_flow.go +++ b/ltml/layout_flow.go @@ -1,10 +1,15 @@ package ltml +import "math" + func LayoutFlow(container Container, style *LayoutStyle, writer Writer) { var cx, cy, maxY float64 rtl := IsRTL(container) containerFull := false - bottom := ContentTop(container) + MaxContentHeight(container) + bottom := math.Inf(1) + if container.Height() != 0 { + bottom = ContentTop(container) + MaxContentHeight(container) + } widgets, remaining := printableWidgets(container, Static) for _, widget := range remaining { widget.SetVisible(false) @@ -15,8 +20,8 @@ func LayoutFlow(container Container, style *LayoutStyle, writer Writer) { continue } if widgetZeroFootprint(widget) { - widget.SetWidth(0) - widget.SetHeight(0) + widget.ResolveWidth(0) + widget.ResolveHeight(0) if rtl { widget.SetLeft(ContentRight(container)) } else { @@ -27,14 +32,14 @@ func LayoutFlow(container Container, style *LayoutStyle, writer Writer) { widget.SetVisible(widget.Top() <= bottom) continue } - if w := widget.Width(); w == 0 { + if widgetAutoWidth(widget) || !widgetWidthSpecified(widget) { pw := widget.PreferredWidth(writer) cw := ContentWidth(container) if pw == 0 { pw = cw } - w = min(pw, cw) - widget.SetWidth(w) + w := min(pw, cw) + widget.ResolveWidth(w) } if cx != 0 && (cx+widget.Width()) > ContentWidth(container) { cy += maxY + style.VPadding() @@ -46,8 +51,8 @@ func LayoutFlow(container Container, style *LayoutStyle, writer Writer) { widget.SetLeft(ContentLeft(container) + cx) } widget.SetTop(ContentTop(container) + cy) - if h := widget.Height(); h == 0 { - widget.SetHeight(widget.PreferredHeight(writer)) + if widgetAutoHeight(widget) || !widgetHeightSpecified(widget) { + widget.ResolveHeight(widget.PreferredHeight(writer)) } widget.LayoutWidget(writer) if widget.Bottom() > bottom { @@ -59,7 +64,7 @@ func LayoutFlow(container Container, style *LayoutStyle, writer Writer) { maxY = max(maxY, widget.Height()) } if container.Height() == 0 && maxY > 0 { - container.SetHeight(cy + maxY + NonContentHeight(container)) + container.ResolveHeight(cy + maxY + NonContentHeight(container)) } layoutPositionedChildren(container, writer) } diff --git a/ltml/layout_flow_test.go b/ltml/layout_flow_test.go new file mode 100644 index 0000000..8ca0f47 --- /dev/null +++ b/ltml/layout_flow_test.go @@ -0,0 +1,128 @@ +package ltml + +import ( + "math" + "testing" + + "github.com/rowland/leadtype/ltml/ltpdf" + "github.com/rowland/leadtype/pdf" +) + +func TestLayoutFlow_RecomputesUnspecifiedCardHeightsAfterVBoxProbe(t *testing.T) { + writer := &labelTestWriter{t: t} + flowStyle := &LayoutStyle{manager: "flow", hpadding: 12, vpadding: 12} + + buildFlow := func() (*StdContainer, []*StdContainer) { + flow := &StdContainer{} + flow.layout = flowStyle + flow.SetLeft(0) + flow.SetTop(0) + flow.SetWidth(550) + flow.SetHeight(300) + + cards := make([]*StdContainer, 0, 5) + for _, contentWidth := range []float64{76, 68, 60, 52, 44} { + card := &StdContainer{} + card.layout = &LayoutStyle{manager: "vbox", vpadding: 8} + card.SetWidth(92) + card.SetAttrs(map[string]string{"padding": "8pt"}) + if err := card.SetContainer(flow); err != nil { + t.Fatal(err) + } + flow.AddChild(card) + + draw := &positionedTestWidget{preferredWidth: contentWidth, preferredHeight: contentWidth} + draw.SetWidth(contentWidth) + if err := draw.SetContainer(card); err != nil { + t.Fatal(err) + } + card.AddChild(draw) + + caption := &positionedTestWidget{preferredWidth: 76, preferredHeight: 12} + caption.SetWidth(76) + if err := caption.SetContainer(card); err != nil { + t.Fatal(err) + } + card.AddChild(caption) + + cards = append(cards, card) + } + + return flow, cards + } + + directFlow, directCards := buildFlow() + LayoutFlow(directFlow, flowStyle, writer) + directHeights := make([]float64, len(directCards)) + directTops := make([]float64, len(directCards)) + directVisible := make([]bool, len(directCards)) + for i, card := range directCards { + directHeights[i] = card.Height() + directTops[i] = card.Top() + directVisible[i] = card.Visible() + } + + probedFlow, probedCards := buildFlow() + outer := positionedContainer(0, 0, 550, 300) + outer.layout = &LayoutStyle{manager: "vbox"} + if err := probedFlow.SetContainer(outer); err != nil { + t.Fatal(err) + } + outer.AddChild(probedFlow) + + LayoutVBox(outer, outer.layout, writer) + + for i, card := range probedCards { + if got := card.Visible(); got != directVisible[i] { + t.Fatalf("card %d visible = %v, want %v", i, got, directVisible[i]) + } + if got := card.Height(); math.Abs(got-directHeights[i]) > 0.001 { + t.Fatalf("card %d height = %v, want %v", i, got, directHeights[i]) + } + if got := card.Top(); math.Abs(got-directTops[i]) > 0.001 { + t.Fatalf("card %d top = %v, want %v", i, got, directTops[i]) + } + } +} + +func TestCanvasSample_PostageStampFlowCardsRemainVisible(t *testing.T) { + doc, err := ParseFile(sampleFile("test_054_canvas_draw.ltml")) + if err != nil { + t.Fatal(err) + } + ttFonts, afmFonts, err := loadSampleFontSources() + if err != nil { + t.Fatal(err) + } + writer := <pdf.DocWriter{DocWriter: pdf.NewDocWriter()} + writer.AddFontSource(ttFonts) + writer.AddFontSource(afmFonts) + if err := doc.Print(writer); err != nil { + t.Fatal(err) + } + + var flow *StdContainer + walkWidgets(doc.Root(), func(widget Widget) bool { + container, ok := widget.(*StdContainer) + if !ok || container.layout == nil || container.layout.manager != "flow" { + return true + } + flow = container + return false + }) + if flow == nil { + t.Fatal("flow container not found in canvas sample") + } + if len(flow.children) != 5 { + t.Fatalf("flow child count = %d, want 5", len(flow.children)) + } + for i, child := range flow.children { + card, ok := child.(*StdContainer) + if !ok { + t.Fatalf("flow child %d type = %T, want *StdContainer", i, child) + } + if !card.Visible() { + t.Fatalf("flow child %d visible = false (top=%v height=%v bottom=%v flowBottom=%v)", i, card.Top(), card.Height(), card.Bottom(), flow.Bottom()) + } + } +} diff --git a/ltml/layout_overflow_test.go b/ltml/layout_overflow_test.go index 071a76c..cd22f36 100644 --- a/ltml/layout_overflow_test.go +++ b/ltml/layout_overflow_test.go @@ -14,6 +14,7 @@ type flowTestWidget struct { preferredWidth float64 preferredHeight float64 printedOn *[]int + printedHeights *[]float64 layoutCalls int } @@ -39,6 +40,9 @@ func (w *flowTestWidget) DrawContent(Writer) error { *w.printedOn = append(*w.printedOn, doc.CurrentPhysicalPageNo()) } } + if w.printedHeights != nil { + *w.printedHeights = append(*w.printedHeights, w.Height()) + } return nil } @@ -316,6 +320,162 @@ func TestStdPage_VBoxOverflowDefaultsToTrue(t *testing.T) { } } +func TestStdPage_FixedHeightVBoxSplitDistributesAutoHeightPerPage(t *testing.T) { + page := &StdPage{pageStyle: &PageStyle{width: 200, height: 100}} + page.layout = defaultLayouts["vbox"].Clone() + page.overflow = true + doc := newFlowPageDoc(page) + + box := &StdContainer{} + _ = box.SetContainer(page) + box.layout = defaultLayouts["vbox"].Clone() + box.SetWidth(180) + box.SetHeight(140) + page.AddChild(box) + + row1 := &flowTestWidget{name: "row1", preferredHeight: 30} + row1.SetHeight(30) + _ = row1.SetContainer(box) + box.AddChild(row1) + + var auto1Pages, auto2Pages, auto3Pages []int + var auto1Heights, auto2Heights, auto3Heights []float64 + + auto1 := &flowTestWidget{name: "auto1", preferredHeight: 10, printedOn: &auto1Pages, printedHeights: &auto1Heights} + auto1.SetHeightAuto() + _ = auto1.SetContainer(box) + box.AddChild(auto1) + + row3 := &flowTestWidget{name: "row3", preferredHeight: 20} + _ = row3.SetContainer(box) + box.AddChild(row3) + + auto2 := &flowTestWidget{name: "auto2", preferredHeight: 10, printedOn: &auto2Pages, printedHeights: &auto2Heights} + auto2.SetHeightAuto() + _ = auto2.SetContainer(box) + box.AddChild(auto2) + + row5 := &flowTestWidget{name: "row5", preferredHeight: 20} + _ = row5.SetContainer(box) + box.AddChild(row5) + + row6 := &flowTestWidget{name: "row6", preferredHeight: 20} + _ = row6.SetContainer(box) + box.AddChild(row6) + + auto3 := &flowTestWidget{name: "auto3", preferredHeight: 10, printedOn: &auto3Pages, printedHeights: &auto3Heights} + auto3.SetHeightAuto() + _ = auto3.SetContainer(box) + box.AddChild(auto3) + + w := &labelTestWriter{t: t} + if err := doc.Print(w); err != nil { + t.Fatal(err) + } + if w.pageCount != 2 { + t.Fatalf("page count = %d, want 2", w.pageCount) + } + if !slices.Equal(auto1Pages, []int{1}) || len(auto1Heights) != 1 || auto1Heights[0] != 15 { + t.Fatalf("auto1 = pages:%v heights:%v, want page 1 height 15", auto1Pages, auto1Heights) + } + if !slices.Equal(auto2Pages, []int{1}) || len(auto2Heights) != 1 || auto2Heights[0] != 15 { + t.Fatalf("auto2 = pages:%v heights:%v, want page 1 height 15", auto2Pages, auto2Heights) + } + if !slices.Equal(auto3Pages, []int{2}) || len(auto3Heights) != 1 || auto3Heights[0] != 80 { + t.Fatalf("auto3 = pages:%v heights:%v, want page 2 height 80", auto3Pages, auto3Heights) + } +} + +func TestStdPage_FixedHeightVBoxSplitOnlySharesOnPagesWithAutoChildren(t *testing.T) { + page := &StdPage{pageStyle: &PageStyle{width: 200, height: 100}} + page.layout = defaultLayouts["vbox"].Clone() + page.overflow = true + doc := newFlowPageDoc(page) + + box := &StdContainer{} + _ = box.SetContainer(page) + box.layout = defaultLayouts["vbox"].Clone() + box.SetWidth(180) + box.SetHeight(140) + page.AddChild(box) + + var row1Heights, row2Heights, autoHeights []float64 + var row1Pages, row2Pages, autoPages []int + + row1 := &flowTestWidget{name: "row1", preferredHeight: 50, printedOn: &row1Pages, printedHeights: &row1Heights} + _ = row1.SetContainer(box) + box.AddChild(row1) + + row2 := &flowTestWidget{name: "row2", preferredHeight: 50, printedOn: &row2Pages, printedHeights: &row2Heights} + _ = row2.SetContainer(box) + box.AddChild(row2) + + auto := &flowTestWidget{name: "auto", preferredHeight: 10, printedOn: &autoPages, printedHeights: &autoHeights} + auto.SetHeightAuto() + _ = auto.SetContainer(box) + box.AddChild(auto) + + row4 := &flowTestWidget{name: "row4", preferredHeight: 20} + _ = row4.SetContainer(box) + box.AddChild(row4) + + w := &labelTestWriter{t: t} + if err := doc.Print(w); err != nil { + t.Fatal(err) + } + if w.pageCount != 2 { + t.Fatalf("page count = %d, want 2", w.pageCount) + } + if !slices.Equal(row1Pages, []int{1}) || len(row1Heights) != 1 || row1Heights[0] != 50 { + t.Fatalf("row1 = pages:%v heights:%v, want page 1 height 50", row1Pages, row1Heights) + } + if !slices.Equal(row2Pages, []int{1}) || len(row2Heights) != 1 || row2Heights[0] != 50 { + t.Fatalf("row2 = pages:%v heights:%v, want page 1 height 50", row2Pages, row2Heights) + } + if !slices.Equal(autoPages, []int{2}) || len(autoHeights) != 1 || autoHeights[0] != 80 { + t.Fatalf("auto = pages:%v heights:%v, want page 2 height 80", autoPages, autoHeights) + } +} + +func TestStdPage_NaturalVBoxSplitLeavesAutoHeightAtPreferredSize(t *testing.T) { + page := &StdPage{pageStyle: &PageStyle{width: 200, height: 100}} + page.layout = defaultLayouts["vbox"].Clone() + page.overflow = true + doc := newFlowPageDoc(page) + + box := &StdContainer{} + _ = box.SetContainer(page) + box.layout = defaultLayouts["vbox"].Clone() + box.SetWidth(180) + page.AddChild(box) + + row1 := &flowTestWidget{name: "row1", preferredHeight: 60} + _ = row1.SetContainer(box) + box.AddChild(row1) + + var autoPages []int + var autoHeights []float64 + auto := &flowTestWidget{name: "auto", preferredHeight: 10, printedOn: &autoPages, printedHeights: &autoHeights} + auto.SetHeightAuto() + _ = auto.SetContainer(box) + box.AddChild(auto) + + row3 := &flowTestWidget{name: "row3", preferredHeight: 60} + _ = row3.SetContainer(box) + box.AddChild(row3) + + w := &labelTestWriter{t: t} + if err := doc.Print(w); err != nil { + t.Fatal(err) + } + if w.pageCount != 2 { + t.Fatalf("page count = %d, want 2", w.pageCount) + } + if !slices.Equal(autoPages, []int{1}) || len(autoHeights) != 1 || autoHeights[0] != 10 { + t.Fatalf("auto = pages:%v heights:%v, want page 1 height 10", autoPages, autoHeights) + } +} + func TestStdPage_FlowOverflowDefaultsToTrue(t *testing.T) { page := &StdPage{pageStyle: &PageStyle{width: 100, height: 45}} page.layout = defaultLayouts["flow"].Clone() diff --git a/ltml/layout_radial.go b/ltml/layout_radial.go index 88ef005..36be2da 100644 --- a/ltml/layout_radial.go +++ b/ltml/layout_radial.go @@ -75,10 +75,10 @@ func LayoutRadialTable(container Container, style *LayoutStyle, writer Writer) { } } if !container.HeightIsSet() { - container.SetHeight((outerRadius * 2) + NonContentHeight(container)) + container.ResolveHeight((outerRadius * 2) + NonContentHeight(container)) } if !container.WidthIsSet() { - container.SetWidth((outerRadius * 2) + NonContentWidth(container)) + container.ResolveWidth((outerRadius * 2) + NonContentWidth(container)) } } @@ -89,12 +89,12 @@ func inferRadialContainerDimensions(container Container) { } if !base.WidthIsSet() { if width, ok := base.radialInferredWidth(); ok { - base.SetWidth(width) + base.ResolveWidth(width) } } if !base.HeightIsSet() { if height, ok := base.radialInferredHeight(); ok { - base.SetHeight(height) + base.ResolveHeight(height) } } } diff --git a/ltml/layout_table.go b/ltml/layout_table.go index 93e7353..0b27474 100644 --- a/ltml/layout_table.go +++ b/ltml/layout_table.go @@ -89,7 +89,7 @@ func detectWidths(grid *WidgetGrid, writer Writer) SpecifiedSizes { widths[c] = &SpecifiedSize{How: Unspecified, Size: 0} } else if widget.WidthPctIsSet() { widths[c] = &SpecifiedSize{How: Percent, Size: widget.Width()} - } else if widget.WidthIsSet() { + } else if widgetWidthSpecified(widget) { widths[c] = &SpecifiedSize{How: Specified, Size: widget.Width()} } else { max := 0.0 @@ -194,7 +194,7 @@ func LayoutTable(container Container, style *LayoutStyle, writer Writer) { for i := 0; i < widget.ColSpan(); i++ { width += widths[c+i].Size } - widget.SetWidth(width + float64(widget.ColSpan()-1)*style.HPadding()) + widget.ResolveWidth(width + float64(widget.ColSpan()-1)*style.HPadding()) var height float64 if widget.HeightIsSet() { height = widget.Height() @@ -265,7 +265,7 @@ func LayoutTable(container Container, style *LayoutStyle, writer Writer) { for rowOffset := 0; rowOffset < ss.Span; rowOffset++ { height += heights.Cell(c, r+rowOffset).Size } - widget.SetHeight(height) + widget.ResolveHeight(height) if ss.Span == 1 && ss.Size > maxHeight { maxHeight = ss.Size } @@ -289,7 +289,7 @@ func LayoutTable(container Container, style *LayoutStyle, writer Writer) { } } if !container.HeightIsSet() { - container.SetHeight(top - ContentTop(container) + NonContentHeight(container) - style.VPadding()) + container.ResolveHeight(top - ContentTop(container) + NonContentHeight(container) - style.VPadding()) } static, remaining := printableWidgets(container, Static) for _, widget := range remaining { diff --git a/ltml/linking.go b/ltml/linking.go index 7ea99da..5fc5eab 100644 --- a/ltml/linking.go +++ b/ltml/linking.go @@ -294,6 +294,8 @@ func (d *StdDocument) resetRenderState() { widget.SetPrinted(false) widget.SetVisible(true) widget.SetDisabled(false) + widget.ClearResolvedWidth() + widget.ClearResolvedHeight() switch value := widget.(type) { case *StdPage: value.flowPageIndex = 0 diff --git a/ltml/ltml_test.go b/ltml/ltml_test.go index de4547a..700e141 100644 --- a/ltml/ltml_test.go +++ b/ltml/ltml_test.go @@ -248,6 +248,8 @@ func TestSamples(t *testing.T) { "test_054_canvas_draw", "test_055_arabic_index", "test_056_hbox_auto_width", + "test_057_vbox_auto_height", + "test_058_vbox_auto_height_overflow", } for _, sample := range samples { diff --git a/ltml/radial_layout_test.go b/ltml/radial_layout_test.go index f81bb82..cb6d678 100644 --- a/ltml/radial_layout_test.go +++ b/ltml/radial_layout_test.go @@ -1006,3 +1006,42 @@ func TestStdSector_WithParagraphChild_DoesNotDefaultToTangentRotation(t *testing t.Fatalf("content rotation = %v, want 0 for paragraph-bearing sector", got) } } + +func TestRadialSample_ImplicitParagraphsKeepLegacyPlacement(t *testing.T) { + doc, err := ParseFile("/Users/brent/src/leadtype/ltml/samples/test_038_radial_layout.ltml") + if err != nil { + t.Fatal(err) + } + + w := &labelTestWriter{t: t} + if err := doc.Print(w); err != nil { + t.Fatal(err) + } + + var direct *StdParagraph + var other *StdParagraph + walkWidgets(doc.Root(), func(widget Widget) bool { + paragraph, ok := widget.(*StdParagraph) + if !ok { + return true + } + text := paragraph.AccessibilityText() + if strings.HasPrefix(text, "This paragraph is a direct child") { + direct = paragraph + } + if strings.HasPrefix(text, "Another implicit sector paragraph") { + other = paragraph + } + return true + }) + + if direct == nil || other == nil { + t.Fatalf("missing implicit paragraphs: direct=%v other=%v", direct != nil, other != nil) + } + if got, want := direct.Top(), 175.03; math.Abs(got-want) > 0.5 { + t.Fatalf("direct implicit paragraph top = %v, want near %v", got, want) + } + if got, want := other.Top(), 386.59; math.Abs(got-want) > 0.5 { + t.Fatalf("secondary implicit paragraph top = %v, want near %v", got, want) + } +} diff --git a/ltml/samples/test_051_paper_sizes.pdf b/ltml/samples/test_051_paper_sizes.pdf index e05a274b79a14550067088c6c68949bcd37bf2bd..5d79c01100b65529c5eeb049ec09719b44689d18 100644 GIT binary patch delta 793 zcmZXSze)o^5XRX%1aqwxHbR^j@KOgnECemzTMa5=xsTAIG(uBKc6ff?sgRqdWub#*Z7(ipzK@m2HlX9*w%B~4l)fhc4U5!or9#G*(V zw--AAi2}IIGUDvRAYtzfQbI0K0x9(Y5`|zwrU3efx=rvDLF`zRfBFn$>PgIK7bU7m zloCXXlp}Pde??+5rlerH))0NsB7iamFj^wPYN_Y4u?_*PGt3#OjD!%pl=bH|mGVf; wcNxnYM^H2+Vm>l$5>0>KU1r{xMUP13Bgja>=zGd;hyJ2E-Q}ijr}OjX3#X5z2LJ#7 delta 783 zcmZXSy-Gtt5QVunjk%RzBPf_A{mks{{3uA8XeEN3tq-8&32tFwV|R;N1fN00!ZP>* zVw*yI3VYX|Kz7__;p5EgoHP4AA1>#^hy6+Sc)QiT9$Y-#Pwrm&*;yt1bzNO`vyYvA zb5NakZ_B-I_mgiHgX(!PsJD;XID+ppx@>-KmI%QiHBBWFNURPKkyiy3j}jTK7q$SB z*kqk$#4O;DFquO}NF`E`vOj=QjLxJ+us_tUjB5h%vLyfdGmwi%nb8+YQs+n&qDRJ& z+j4$GN={~!^LMJ4LXjeXF$Q)jk>Gn-;4-lm0sUkMBve|;n!siJ2+6p767w!oxp4$T q7b4~-W#wr4`|mOfZdeStu>1rW8Myv|@<#gpq(0oBW;~vr9yMPho}|10 diff --git a/ltml/samples/test_057_vbox_auto_height.ltml b/ltml/samples/test_057_vbox_auto_height.ltml new file mode 100644 index 0000000..d91be08 --- /dev/null +++ b/ltml/samples/test_057_vbox_auto_height.ltml @@ -0,0 +1,75 @@ + + + + + + + + + + + + +

+ When a fixed-height vbox has surplus room, omitted heights stay at their + preferred size and only height="auto" children grow. In a constrained + fixed-height vbox, auto behaves like an omitted height. +

+ +
Roomy fixed-height container
+
+ + + + +
+ +
Constrained fixed-height container
+
+ + + +
+
+
diff --git a/ltml/samples/test_057_vbox_auto_height.pdf b/ltml/samples/test_057_vbox_auto_height.pdf new file mode 100644 index 0000000000000000000000000000000000000000..53c085e813575faba7598b63c42514f53509326f GIT binary patch literal 35949 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 literal 0 HcmV?d00001 diff --git a/ltml/samples/test_058_vbox_auto_height_overflow.ltml b/ltml/samples/test_058_vbox_auto_height_overflow.ltml new file mode 100644 index 0000000..64832e8 --- /dev/null +++ b/ltml/samples/test_058_vbox_auto_height_overflow.ltml @@ -0,0 +1,55 @@ + + + + + + + + + + + +

+ This fixed-height vbox splits across pages. The first fragment contains + only fixed-height children. The continuation fragment contains an omitted + height and a height="auto" child, so only that page shares surplus height + with the auto child. +

+ +
+ + + + + +
+
+
diff --git a/ltml/samples/test_058_vbox_auto_height_overflow.pdf b/ltml/samples/test_058_vbox_auto_height_overflow.pdf new file mode 100644 index 0000000000000000000000000000000000000000..a6a70acceb18b36f864e6b4b7ea4eb2577cf9157 GIT binary patch literal 33039 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 JURG!Le*mPNrzZda literal 0 HcmV?d00001 diff --git a/ltml/std_container.go b/ltml/std_container.go index fc002db..4ad0e38 100644 --- a/ltml/std_container.go +++ b/ltml/std_container.go @@ -106,8 +106,8 @@ func (c *StdContainer) Widgets() []Widget { } func (c *StdContainer) PreferredHeight(w Writer) float64 { - if c.height != 0 { - return float64(c.height) + if c.HeightIsSet() { + return c.Height() } c.prepareForLayout(w) if isRadialLayoutStyle(c.layout) { @@ -118,13 +118,20 @@ func (c *StdContainer) PreferredHeight(w Writer) float64 { saved := c.SaveState() LayoutContainer(c, newLayoutProbeWriter(w)) height := c.Height() + walkWidgets(c, func(widget Widget) bool { + if widget != c { + widget.ClearResolvedWidth() + widget.ClearResolvedHeight() + } + return true + }) c.RestoreState(saved) return height } func (c *StdContainer) PreferredWidth(w Writer) float64 { - if c.width != 0 { - return float64(c.width) + if c.WidthIsSet() { + return c.Width() } if isRadialLayoutStyle(c.layout) { if width, ok := c.radialInferredWidth(); ok { @@ -446,9 +453,9 @@ func (c *StdContainer) tableSplitMetrics(w Writer) (*tableSplitMetrics, error) { for i := 0; i < widget.ColSpan(); i++ { width += widths[col+i].Size } - widget.SetWidth(width + float64(widget.ColSpan()-1)*c.LayoutStyle().HPadding()) + widget.ResolveWidth(width + float64(widget.ColSpan()-1)*c.LayoutStyle().HPadding()) height := widget.Height() - if !widget.HeightIsSet() { + if !widgetHeightSpecified(widget) { height = widget.PreferredHeight(w) } if height > maxHeight { @@ -499,7 +506,8 @@ func (c *StdContainer) tableFragmentHeight(metrics *tableSplitMetrics, bodyStart func (c *StdContainer) cloneTableFragment(metrics *tableSplitMetrics, rows []int) *StdContainer { clone := *c clone.activeChildren = c.cloneTableWidgetsForRows(metrics.grid, rows, &clone) - clone.ClearHeight() + clone.ClearResolvedWidth() + clone.ClearResolvedHeight() clone.printed = false clone.invisible = false clone.disabled = false @@ -753,7 +761,7 @@ func (c *StdContainer) vboxSplitMetrics(w Writer) *vboxSplitMetrics { } rtl := IsRTL(c) for _, child := range static { - if !child.WidthIsSet() { + if widgetAutoWidth(child) || !widgetWidthSpecified(child) { cw := ContentWidth(c) pw := 0.0 if _, ok := child.(*StdParagraph); ok { @@ -764,11 +772,11 @@ func (c *StdContainer) vboxSplitMetrics(w Writer) *vboxSplitMetrics { if pw == 0 { pw = cw } - child.SetWidth(min(pw, cw)) + child.ResolveWidth(min(pw, cw)) } child.SetLeft(vboxCrossAxisLeft(c, child, rtl)) height := child.Height() - if !child.HeightIsSet() { + if !widgetHeightSpecified(child) { height = child.PreferredHeight(w) } metrics.heights[child] = height @@ -848,7 +856,8 @@ func (c *StdContainer) vboxHasVisibleWidget(groups ...[]Widget) bool { func (c *StdContainer) cloneVBoxFragment(included map[Widget]bool, replacements map[Widget]Widget) *StdContainer { clone := *c clone.activeChildren = make([]Widget, 0, len(included)+len(replacements)) - clone.ClearHeight() + clone.ClearResolvedWidth() + clone.ClearResolvedHeight() clone.printed = false clone.invisible = false clone.disabled = false @@ -890,6 +899,8 @@ func cloneWidgetShallow(widget Widget) Widget { if !ok { panic("cloneWidgetShallow produced non-widget clone") } + w.ClearResolvedWidth() + w.ClearResolvedHeight() w.SetPrinted(false) w.SetVisible(true) w.SetDisabled(false) diff --git a/ltml/std_index.go b/ltml/std_index.go index 774dfee..d2847c2 100644 --- a/ltml/std_index.go +++ b/ltml/std_index.go @@ -5,8 +5,6 @@ import "fmt" type StdIndex struct { StdContainer expandedTargets []string - explicitWidth bool - explicitHeight bool expandErr error } @@ -95,12 +93,6 @@ func (i *StdIndex) SplitForHeight(avail float64, w Writer) (*SplitResult, error) return &SplitResult{Head: head, Tail: tail}, nil } -func (i *StdIndex) SetAttrs(attrs map[string]string) { - i.StdContainer.SetAttrs(attrs) - i.explicitWidth = MapHasAnyKey(attrs, "width") || (i.sides[leftSide].IsSet && i.sides[rightSide].IsSet) - i.explicitHeight = MapHasAnyKey(attrs, "height") || (i.sides[topSide].IsSet && i.sides[bottomSide].IsSet) -} - func (i *StdIndex) clearExpandedState() { i.activeChildren = nil i.expandedTargets = nil @@ -108,12 +100,8 @@ func (i *StdIndex) clearExpandedState() { } func (i *StdIndex) clearMeasuredGeometry() { - if !i.explicitWidth { - i.ClearWidth() - } - if !i.explicitHeight { - i.ClearHeight() - } + i.ClearResolvedWidth() + i.ClearResolvedHeight() } func (i *StdIndex) currentEntries() []resolvedIndexEntry { diff --git a/ltml/std_page.go b/ltml/std_page.go index 74a06db..0e2dfc3 100644 --- a/ltml/std_page.go +++ b/ltml/std_page.go @@ -47,13 +47,6 @@ func (p *StdPage) BottomIsSet() bool { return true } -func (p *StdPage) root() *StdPage { - if p.container == nil { - return p - } - return p.container.(*StdPage) -} - func (p *StdPage) document() *StdDocument { if p.container != nil { return p.container.(*StdDocument) @@ -201,7 +194,7 @@ func (p *StdPage) SetAttrs(attrs map[string]string) { p.overflowSet = true p.overflow = overflow == "true" } - for k, _ := range attrs { + for k := range attrs { if reMargin.MatchString(k) { p.marginChanged = true break @@ -517,6 +510,8 @@ func (p *StdPage) resetWidgetRenderState(widget Widget) { widget.SetPrinted(false) widget.SetVisible(true) widget.SetDisabled(false) + widget.ClearResolvedWidth() + widget.ClearResolvedHeight() container, ok := widget.(Container) if !ok { return @@ -585,10 +580,14 @@ func (p *StdPage) trySplitChild(item *pageItem, child Widget, w Writer) (bool, e if wc, ok := result.Head.(WantsContainer); ok { _ = wc.SetContainer(p) } + p.resetWidgetRenderState(result.Head) p.copySplitGeometry(result.Head, child) + if widgetAutoHeight(child) || widgetHeightSpecified(child) { + result.Head.ResolveHeight(p.availableHeightForChild(child)) + } result.Head.LayoutWidget(w) if !result.Head.HeightIsSet() { - result.Head.SetHeight(result.Head.PreferredHeight(w)) + result.Head.ResolveHeight(result.Head.PreferredHeight(w)) } if result.Tail != nil { if wc, ok := result.Tail.(WantsContainer); ok { @@ -608,7 +607,7 @@ func (p *StdPage) trySplitChild(item *pageItem, child Widget, w Writer) (bool, e func (p *StdPage) copySplitGeometry(dst, src Widget) { dst.SetLeft(src.Left()) dst.SetTop(src.Top()) - dst.SetWidth(src.Width()) + dst.ResolveWidth(src.Width()) dst.SetPosition(src.Position()) dst.SetVisible(true) dst.SetDisabled(false) diff --git a/ltml/std_paragraph.go b/ltml/std_paragraph.go index 42f23c7..a7b6f79 100644 --- a/ltml/std_paragraph.go +++ b/ltml/std_paragraph.go @@ -272,7 +272,8 @@ func (p *StdParagraph) cloneForSplit(lines []*rich_text.RichText, suppressBullet clone.splitLines = append([]*rich_text.RichText(nil), lines...) clone.suppressBullet = suppressBullet clone.continuationIndent = continuationIndent - clone.ClearHeight() + clone.ClearResolvedWidth() + clone.ClearResolvedHeight() clone.richText = nil clone.printed = false clone.invisible = false diff --git a/ltml/std_widget.go b/ltml/std_widget.go index 67621b9..7214fff 100644 --- a/ltml/std_widget.go +++ b/ltml/std_widget.go @@ -762,33 +762,39 @@ func (widget *StdWidget) Units() Units { } func (widget *StdWidget) Width() float64 { + if widget.widthValid { + return float64(widget.width) + } switch widget.widthMode { case DimPct: return float64(widget.widthValue) / 100.0 * ContentWidth(widget.container) case DimRel: return ContentWidth(widget.container) + float64(widget.widthValue) - case DimSpecified: - return float64(widget.width) + case DimLiteral: + return float64(widget.widthValue) } if widget.sides[leftSide].IsSet && widget.sides[rightSide].IsSet { return widget.resolveRight(widget.sides[rightSide].Float64()) - widget.resolveLeft(widget.sides[leftSide].Float64()) } - return float64(widget.width) + return 0 } func (widget *StdWidget) Height() float64 { + if widget.heightValid { + return float64(widget.height) + } switch widget.heightMode { case DimPct: return float64(widget.heightValue) / 100.0 * ContentHeight(widget.container) case DimRel: return ContentHeight(widget.container) + float64(widget.heightValue) - case DimSpecified: - return float64(widget.height) + case DimLiteral: + return float64(widget.heightValue) } if widget.sides[topSide].IsSet && widget.sides[bottomSide].IsSet { return widget.resolveBottom(widget.sides[bottomSide].Float64()) - widget.resolveTop(widget.sides[topSide].Float64()) } - return float64(widget.height) + return 0 } func (widget *StdWidget) HeightIsSet() bool { diff --git a/ltml/widget.go b/ltml/widget.go index 7c86adc..84fc5cc 100644 --- a/ltml/widget.go +++ b/ltml/widget.go @@ -38,10 +38,14 @@ type Widget interface { SetHeightAuto() SetHeightPct(value float64) SetHeightRel(value float64) + ResolveHeight(value float64) + ClearResolvedHeight() SetWidth(value float64) SetWidthAuto() SetWidthPct(value float64) SetWidthRel(value float64) + ResolveWidth(value float64) + ClearResolvedWidth() Height() float64 HeightIsSet() bool From 54cf6b24c5dd6cc43840ac966a44a4e9677bbf6d Mon Sep 17 00:00:00 2001 From: Brent Rowland Date: Sat, 9 May 2026 10:43:36 -0700 Subject: [PATCH 4/5] Add table auto sizing support --- ltml/SYNTAX.md | 26 +- ltml/dimensions_test.go | 34 +- ltml/layout_overflow_test.go | 27 ++ ltml/layout_table.go | 317 +++++++++++++------ ltml/layout_table_test.go | 276 ++++++++++++++++ ltml/ltml_test.go | 3 + ltml/samples/test_059_table_auto_width.ltml | 85 +++++ ltml/samples/test_059_table_auto_width.pdf | Bin 0 -> 36711 bytes ltml/samples/test_060_table_auto_height.ltml | 78 +++++ ltml/samples/test_060_table_auto_height.pdf | Bin 0 -> 37123 bytes ltml/samples/test_061_table_auto_split.ltml | 76 +++++ ltml/samples/test_061_table_auto_split.pdf | Bin 0 -> 43608 bytes ltml/std_container.go | 24 +- 13 files changed, 815 insertions(+), 131 deletions(-) create mode 100644 ltml/layout_table_test.go create mode 100644 ltml/samples/test_059_table_auto_width.ltml create mode 100644 ltml/samples/test_059_table_auto_width.pdf create mode 100644 ltml/samples/test_060_table_auto_height.ltml create mode 100644 ltml/samples/test_060_table_auto_height.pdf create mode 100644 ltml/samples/test_061_table_auto_split.ltml create mode 100644 ltml/samples/test_061_table_auto_split.pdf diff --git a/ltml/SYNTAX.md b/ltml/SYNTAX.md index 28e97bf..96b0a3d 100644 --- a/ltml/SYNTAX.md +++ b/ltml/SYNTAX.md @@ -1191,8 +1191,8 @@ not automatically change paragraph shaping or bidi behavior inside text widgets. width beyond the preferred widths of the remaining unsized children plus hpadding, omitted widths keep their preferred widths and the `auto` children split the leftover space evenly. -- In constrained hboxes, and in other layout managers, `width="auto"` behaves - the same as an omitted width. +- In constrained hboxes, and in layout managers without their own `auto` + sizing policy, `width="auto"` behaves the same as an omitted width. - In `dir="rtl"`, stacking order reverses: children flow right to left, `align="left"` pins to the right side, and `align="right"` pins to the left side. ### Table Details @@ -1201,7 +1201,25 @@ not automatically change paragraph shaping or bidi behavior inside text widgets. - Set `rows` for column-major order (`order="cols"`). - Use `colspan` and `rowspan` attributes on cells to span multiple slots. - Column widths can be fixed (`width="120pt"`), percentage (`width="40%"`), or - automatic (equal share of remaining space). + omitted. Omitted columns keep the historical table behavior: they share the + remaining width equally. +- When at least one single-column cell uses `width="auto"` and the table has + true surplus width beyond the preferred widths of omitted and auto columns, + omitted columns keep their preferred widths and auto columns split the + remaining width evenly. In constrained tables where omitted preferred widths + can still fit, omitted columns keep those preferred widths and auto columns + split the remaining width. Only when omitted preferred widths cannot fit does + the table fall back to equal sharing. +- Cells with `colspan > 1` receive the resolved width of their spanned columns + but do not drive auto column sizing. +- When at least one row contains a `height="auto"` cell and a height-constrained + table has true surplus height beyond fixed and preferred row heights, omitted + rows keep their preferred heights and auto rows split the remaining height + evenly. +- In natural-height tables, and in constrained tables without surplus, + `height="auto"` behaves the same as an omitted height. +- When a direct page-child table splits across pages, auto row height is + evaluated separately for each fragment page. - In `dir="rtl"`, columns are placed right to left (column 0 at the right edge). ### Flow Details @@ -1368,7 +1386,7 @@ Measurements can be expressed in several forms: | With unit | `1in`, `2.5cm`, `14pt` | Explicit unit overrides the current `units`. | | Percentage | `50%` | Percentage of the container's content width or height. | | Relative | `+10`, `-5` | Offset from the container's content dimension. | -| Auto | `auto` | Automatic layout-managed size. Currently special-cased for `hbox` width and `vbox` height; elsewhere it behaves like omitting the dimension. | +| Auto | `auto` | Automatic layout-managed size. Supported by `hbox` width, `vbox` height, table column width, and table row height; elsewhere it behaves like omitting the dimension. | **Supported units:** `pt` (points), `in` (inches, 72pt), `cm` (centimeters, 28.35pt). diff --git a/ltml/dimensions_test.go b/ltml/dimensions_test.go index 4b99694..1186b32 100644 --- a/ltml/dimensions_test.go +++ b/ltml/dimensions_test.go @@ -119,7 +119,7 @@ func TestStdWidget_DimensionResolution(t *testing.T) { } } -func TestDetectWidths_PreservesPercentClassification(t *testing.T) { +func TestDetectTableColumnTracks_PreservesPercentClassification(t *testing.T) { page := &StdPage{pageStyle: &PageStyle{width: 200, height: 120}} grid := NewWidgetGrid(2, 1) @@ -133,36 +133,36 @@ func TestDetectWidths_PreservesPercentClassification(t *testing.T) { specified.SetWidth(80) grid.SetCell(1, 0, specified) - widths := detectWidths(grid, nil) - if got := widths[0].How; got != Percent { - t.Fatalf("widths[0].How = %v, want %v", got, Percent) + tracks := detectTableColumnTracks(grid, nil) + if got := tracks[0].kind; got != tableTrackPercent { + t.Fatalf("tracks[0].kind = %v, want %v", got, tableTrackPercent) } - if got := widths[0].Size; got != 80 { - t.Fatalf("widths[0].Size = %v, want 80", got) + if got := tracks[0].size; got != 80 { + t.Fatalf("tracks[0].size = %v, want 80", got) } - if got := widths[1].How; got != Specified { - t.Fatalf("widths[1].How = %v, want %v", got, Specified) + if got := tracks[1].kind; got != tableTrackSpecified { + t.Fatalf("tracks[1].kind = %v, want %v", got, tableTrackSpecified) } - if got := widths[1].Size; got != 80 { - t.Fatalf("widths[1].Size = %v, want 80", got) + if got := tracks[1].size; got != 80 { + t.Fatalf("tracks[1].size = %v, want 80", got) } } -func TestDetectWidths_TreatsAutoAsUnspecified(t *testing.T) { +func TestDetectTableColumnTracks_ClassifiesAuto(t *testing.T) { page := &StdPage{pageStyle: &PageStyle{width: 200, height: 120}} grid := NewWidgetGrid(1, 1) - auto := &StdWidget{} + auto := &positionedTestWidget{preferredWidth: 35} _ = auto.SetContainer(page) auto.SetWidthAuto() grid.SetCell(0, 0, auto) - widths := detectWidths(grid, nil) - if got := widths[0].How; got != Unspecified { - t.Fatalf("widths[0].How = %v, want %v", got, Unspecified) + tracks := detectTableColumnTracks(grid, nil) + if got := tracks[0].kind; got != tableTrackAuto { + t.Fatalf("tracks[0].kind = %v, want %v", got, tableTrackAuto) } - if got := widths[0].Size; got != 0 { - t.Fatalf("widths[0].Size = %v, want 0", got) + if got := tracks[0].preferred; got != 35 { + t.Fatalf("tracks[0].preferred = %v, want 35", got) } } diff --git a/ltml/layout_overflow_test.go b/ltml/layout_overflow_test.go index cd22f36..b26f174 100644 --- a/ltml/layout_overflow_test.go +++ b/ltml/layout_overflow_test.go @@ -1432,6 +1432,33 @@ func TestSample_TableSplitHeadersFooters_RepeatsTableFooterRows(t *testing.T) { } } +func TestSample_TableAutoSplit_RendersMultiplePages(t *testing.T) { + doc, err := ParseFile(sampleFile("test_061_table_auto_split.ltml")) + if err != nil { + t.Fatal(err) + } + w := &labelTestWriter{t: t} + if err := doc.Print(w); err != nil { + t.Fatal(err) + } + if w.pageCount < 2 { + t.Fatalf("page count = %d, want at least 2 (printed=%q plain=%q)", w.pageCount, joinRichTexts(w.printed), strings.Join(w.plainPrinted, "\n")) + } + pageTexts := map[int][]string{} + for i, rt := range w.printed { + pageTexts[w.printedPages[i]] = append(pageTexts[w.printedPages[i]], rt.String()) + } + page1Text := strings.Join(pageTexts[1], "\n") + page2Text := strings.Join(pageTexts[2], "\n") + if !strings.Contains(page1Text, "Repeating table header") || !strings.Contains(page2Text, "Repeating table header") { + t.Fatalf("expected repeating table header on first two pages, got page1=%q page2=%q", page1Text, page2Text) + } + allText := strings.Join(w.plainPrinted, "\n") + "\n" + joinRichTexts(w.printed) + if !strings.Contains(allText, "row 10 A") || !strings.Contains(allText, "row 10 B") { + t.Fatalf("expected final table row to render, got %q", allText) + } +} + func joinRichTexts(texts []*rich_text.RichText) string { parts := make([]string, 0, len(texts)) for _, text := range texts { diff --git a/ltml/layout_table.go b/ltml/layout_table.go index 0b27474..d69fb7b 100644 --- a/ltml/layout_table.go +++ b/ltml/layout_table.go @@ -5,6 +5,23 @@ import ( "math" ) +type tableTrackKind int8 + +const ( + tableTrackOmitted tableTrackKind = iota + tableTrackSpecified + tableTrackPercent + tableTrackAuto +) + +type tableTrackSize struct { + kind tableTrackKind + size float64 + preferred float64 +} + +type tableTrackPlan []tableTrackSize + func markGrid(grid *BoolGrid, a, b, c, d int, value bool) { for aa := 0; aa < c; aa++ { for bb := 0; bb < d; bb++ { @@ -75,136 +92,186 @@ func colGrid(container Container) (*WidgetGrid, error) { return grid, nil } -func detectWidths(grid *WidgetGrid, writer Writer) SpecifiedSizes { - widths := make(SpecifiedSizes, grid.Cols()) +func detectTableColumnTracks(grid *WidgetGrid, writer Writer) tableTrackPlan { + tracks := make(tableTrackPlan, grid.Cols()) for c := 0; c < grid.Cols(); c++ { - var widget Widget + var specifiedWidget Widget + auto := false + preferred := 0.0 for r := 0; r < grid.Rows(); r++ { if w := grid.Cell(c, r); w != nil && w.ColSpan() == 1 { - widget = w - break + if specifiedWidget == nil && (w.WidthPctIsSet() || widgetWidthSpecified(w)) { + specifiedWidget = w + } + if !widgetWidthSpecified(w) && !w.WidthPctIsSet() { + preferred = max(preferred, w.PreferredWidth(writer)) + } + if widgetAutoWidth(w) { + auto = true + } } } - if widget == nil { - widths[c] = &SpecifiedSize{How: Unspecified, Size: 0} - } else if widget.WidthPctIsSet() { - widths[c] = &SpecifiedSize{How: Percent, Size: widget.Width()} - } else if widgetWidthSpecified(widget) { - widths[c] = &SpecifiedSize{How: Specified, Size: widget.Width()} + if specifiedWidget != nil && specifiedWidget.WidthPctIsSet() { + tracks[c] = tableTrackSize{kind: tableTrackPercent, size: specifiedWidget.Width()} + } else if specifiedWidget != nil { + tracks[c] = tableTrackSize{kind: tableTrackSpecified, size: specifiedWidget.Width()} + } else if auto { + tracks[c] = tableTrackSize{kind: tableTrackAuto, preferred: preferred} } else { - max := 0.0 - for r := 0; r < grid.Rows(); r++ { - if w := grid.Cell(c, r); w != nil { - pw := w.PreferredWidth(writer) - if pw > max { - max = pw - } - } - } - widths[c] = &SpecifiedSize{How: Unspecified, Size: max} + tracks[c] = tableTrackSize{kind: tableTrackOmitted, preferred: preferred} } } - return widths + return tracks } -func allocateSpecifiedWidths(widthAvail float64, specified SpecifiedSizes, style *LayoutStyle) float64 { - for _, w := range specified { - if widthAvail >= w.Size { - widthAvail -= (w.Size + style.HPadding()) - } +func (tracks tableTrackPlan) resolvedSizes() []float64 { + sizes := make([]float64, len(tracks)) + for i, track := range tracks { + sizes[i] = track.size } - return widthAvail + return sizes } -func allocatePercentWidths(widthAvail float64, percents SpecifiedSizes, style *LayoutStyle) float64 { - if widthAvail-(float64(len(percents)-1))*style.HPadding() >= float64(len(percents)) { - widthAvail -= float64((len(percents) - 1)) * style.HPadding() +func allocateTableColumnTracks(widthAvail float64, tracks tableTrackPlan, style *LayoutStyle) { + for i := range tracks { + if tracks[i].kind == tableTrackSpecified && widthAvail >= tracks[i].size { + widthAvail -= tracks[i].size + style.HPadding() + } + } + + var percentIndexes []int + for i, track := range tracks { + if track.kind == tableTrackPercent { + percentIndexes = append(percentIndexes, i) + } + } + if len(percentIndexes) > 0 && widthAvail-(float64(len(percentIndexes)-1))*style.HPadding() >= float64(len(percentIndexes)) { + widthAvail -= float64(len(percentIndexes)-1) * style.HPadding() totalPercents := 0.0 - for i := range percents { - totalPercents += percents[i].Size + for _, i := range percentIndexes { + totalPercents += tracks[i].size } ratio := widthAvail / totalPercents - for i := range percents { + for _, i := range percentIndexes { if ratio < 1.0 { - percents[i].Size *= ratio + tracks[i].size *= ratio } - widthAvail -= percents[i].Size + widthAvail -= tracks[i].size } - } else { - for i := range percents { - percents[i].Size = 0 + widthAvail -= style.HPadding() + } else if len(percentIndexes) > 0 { + for _, i := range percentIndexes { + tracks[i].size = 0 } + widthAvail -= style.HPadding() } - widthAvail -= style.HPadding() - return widthAvail -} -func allocateOtherWidths(widthAvail float64, others SpecifiedSizes, style *LayoutStyle) float64 { - if widthAvail-(float64(len(others)-1))*style.HPadding() >= float64(len(others)) { - widthAvail -= float64(len(others)-1) * style.HPadding() - othersWidth := widthAvail / float64(len(others)) - for i := range others { - others[i].Size = othersWidth + var omittedIndexes, autoIndexes []int + for i, track := range tracks { + switch track.kind { + case tableTrackOmitted: + omittedIndexes = append(omittedIndexes, i) + case tableTrackAuto: + autoIndexes = append(autoIndexes, i) + } + } + otherCount := len(omittedIndexes) + len(autoIndexes) + if otherCount == 0 { + return + } + paddingCost := float64(otherCount-1) * style.HPadding() + if len(autoIndexes) > 0 { + preferredTotal := 0.0 + omittedPreferredTotal := 0.0 + for _, i := range omittedIndexes { + preferredTotal += tracks[i].preferred + omittedPreferredTotal += tracks[i].preferred + } + for _, i := range autoIndexes { + preferredTotal += tracks[i].preferred + } + if widthAvail > preferredTotal+paddingCost { + widthAvail -= paddingCost + for _, i := range omittedIndexes { + tracks[i].size = tracks[i].preferred + widthAvail -= tracks[i].size + } + autoWidth := widthAvail / float64(len(autoIndexes)) + for _, i := range autoIndexes { + tracks[i].size = autoWidth + } + return + } + if widthAvail-paddingCost-omittedPreferredTotal >= float64(len(autoIndexes)) { + widthAvail -= paddingCost + for _, i := range omittedIndexes { + tracks[i].size = tracks[i].preferred + widthAvail -= tracks[i].size + } + autoWidth := widthAvail / float64(len(autoIndexes)) + for _, i := range autoIndexes { + tracks[i].size = autoWidth + } + return + } + } + if widthAvail-paddingCost >= float64(otherCount) { + widthAvail -= paddingCost + otherWidth := widthAvail / float64(otherCount) + for _, i := range omittedIndexes { + tracks[i].size = otherWidth + } + for _, i := range autoIndexes { + tracks[i].size = otherWidth } } else { - for i := range others { - others[i].Size = 0 + for _, i := range omittedIndexes { + tracks[i].size = 0 + } + for _, i := range autoIndexes { + tracks[i].size = 0 } } - return widthAvail } -func LayoutTable(container Container, style *LayoutStyle, writer Writer) { - var grid *WidgetGrid - var err error - - if container.Order() == TableOrderRows { - grid, err = rowGrid(container) - } else if container.Order() == TableOrderCols { - grid, err = colGrid(container) - } else { - panic("invalid order") - } - if err != nil { - panic(err) - } +func planTableColumnWidths(grid *WidgetGrid, container Container, style *LayoutStyle, writer Writer) tableTrackPlan { + tracks := detectTableColumnTracks(grid, writer) + allocateTableColumnTracks(ContentWidth(container), tracks, style) + return tracks +} - containerFull := false - widths := detectWidths(grid, writer) - if container.Width() <= 0 { - panic("container width not set") +func tableCellWidth(widths []float64, startCol, colSpan int, hpadding float64) float64 { + width := 0.0 + for i := 0; i < colSpan; i++ { + width += widths[startCol+i] } - percents, others := widths.Partition(func(w *SpecifiedSize) bool { return w.How == Percent }) - specified, others := others.Partition(func(w *SpecifiedSize) bool { return w.How == Specified }) - - widthAvail := ContentWidth(container) - widthAvail = allocateSpecifiedWidths(widthAvail, specified, style) - widthAvail = allocatePercentWidths(widthAvail, percents, style) - widthAvail = allocateOtherWidths(widthAvail, others, style) + return width + float64(colSpan-1)*hpadding +} +func tableBaseHeights(grid *WidgetGrid, widths []float64, style *LayoutStyle, writer Writer) (*SpanSizeGrid, []bool) { heights := NewSpanSizeGrid(grid.Cols(), grid.Rows()) + autoRows := make([]bool, grid.Rows()) for c := 0; c < grid.Cols(); c++ { for r := 0; r < grid.Rows(); r++ { widget := grid.Cell(c, r) if widget == nil { continue } - if widths[c].Size > 0 { - width := 0.0 - for i := 0; i < widget.ColSpan(); i++ { - width += widths[c+i].Size - } - widget.ResolveWidth(width + float64(widget.ColSpan()-1)*style.HPadding()) - var height float64 - if widget.HeightIsSet() { - height = widget.Height() - } else { - height = widget.PreferredHeight(writer) - } - heights.SetCell(c, r, SpanSize{Span: widget.RowSpan(), Size: height}) - } else { + if widths[c] <= 0 { widget.SetDisabled(true) + continue + } + widget.ResolveWidth(tableCellWidth(widths, c, widget.ColSpan(), style.HPadding())) + var height float64 + if widget.HeightIsSet() { + height = widget.Height() + } else { + height = widget.PreferredHeight(writer) + if widgetAutoHeight(widget) && widget.RowSpan() == 1 { + autoRows[r] = true + } } + heights.SetCell(c, r, SpanSize{Span: widget.RowSpan(), Size: height}) } } @@ -230,6 +297,66 @@ func LayoutTable(container Container, style *LayoutStyle, writer Writer) { heights.SetCell(c, r, ss) } } + return heights, autoRows +} + +func applyTableAutoRowHeights(container Container, style *LayoutStyle, heights *SpanSizeGrid, autoRows []bool) { + if !container.HeightIsSet() { + return + } + autoCount := 0 + baselineHeight := 0.0 + for r := 0; r < heights.Rows(); r++ { + if r > 0 { + baselineHeight += style.VPadding() + } + baselineHeight += heights.Cell(0, r).Size + if autoRows[r] { + autoCount++ + } + } + if autoCount == 0 { + return + } + surplus := ContentHeight(container) - baselineHeight + if surplus <= 0 { + return + } + extra := surplus / float64(autoCount) + for r := 0; r < heights.Rows(); r++ { + if !autoRows[r] { + continue + } + for c := 0; c < heights.Cols(); c++ { + ss := heights.Cell(c, r) + ss.Size += extra + heights.SetCell(c, r, ss) + } + } +} + +func LayoutTable(container Container, style *LayoutStyle, writer Writer) { + var grid *WidgetGrid + var err error + + if container.Order() == TableOrderRows { + grid, err = rowGrid(container) + } else if container.Order() == TableOrderCols { + grid, err = colGrid(container) + } else { + panic("invalid order") + } + if err != nil { + panic(err) + } + + containerFull := false + if container.Width() <= 0 { + panic("container width not set") + } + widths := planTableColumnWidths(grid, container, style, writer).resolvedSizes() + heights, autoRows := tableBaseHeights(grid, widths, style, writer) + applyTableAutoRowHeights(container, style, heights, autoRows) top := ContentTop(container) bottom := top + MaxContentHeight(container) @@ -253,11 +380,7 @@ func LayoutTable(container Container, style *LayoutStyle, writer Writer) { ss := heights.Cell(c, r) widget.SetTop(top) if rtl { - colWidth := widths[c].Size - for i := 1; i < widget.ColSpan(); i++ { - colWidth += widths[c+i].Size + style.HPadding() - } - widget.SetLeft(right - colWidth) + widget.SetLeft(right - tableCellWidth(widths, c, widget.ColSpan(), style.HPadding())) } else { widget.SetLeft(left) } @@ -270,8 +393,8 @@ func LayoutTable(container Container, style *LayoutStyle, writer Writer) { maxHeight = ss.Size } } - left += widths[c].Size + style.HPadding() - right -= widths[c].Size + style.HPadding() + left += widths[c] + style.HPadding() + right -= widths[c] + style.HPadding() } if containerFull { continue diff --git a/ltml/layout_table_test.go b/ltml/layout_table_test.go new file mode 100644 index 0000000..7677c19 --- /dev/null +++ b/ltml/layout_table_test.go @@ -0,0 +1,276 @@ +package ltml + +import "testing" + +func testTableContainer(width, height float64, cols int) *StdContainer { + page := &StdPage{pageStyle: &PageStyle{width: 400, height: 400}} + c := &StdContainer{} + _ = c.SetContainer(page) + c.SetWidth(width) + if height > 0 { + c.SetHeight(height) + } + c.cols = cols + c.order = TableOrderRows + c.layout = defaultLayouts["table"].Clone() + return c +} + +func addTableTestWidget(t *testing.T, c *StdContainer, preferredWidth, preferredHeight float64) *positionedTestWidget { + t.Helper() + w := &positionedTestWidget{preferredWidth: preferredWidth, preferredHeight: preferredHeight} + if err := w.SetContainer(c); err != nil { + t.Fatal(err) + } + c.AddChild(w) + return w +} + +func TestLayoutTable_AutoWidthUsesSurplusWithoutChangingOmittedColumns(t *testing.T) { + c := testTableContainer(300, 80, 4) + + fixed := addTableTestWidget(t, c, 40, 20) + fixed.SetWidth(60) + + omitted := addTableTestWidget(t, c, 35, 20) + autoA := addTableTestWidget(t, c, 40, 20) + autoA.SetWidthAuto() + autoB := addTableTestWidget(t, c, 30, 20) + autoB.SetWidthAuto() + + LayoutTable(c, c.LayoutStyle(), &labelTestWriter{t: t}) + + if got := fixed.Width(); got != 60 { + t.Fatalf("fixed.Width() = %v, want 60", got) + } + if got := omitted.Width(); got != 35 { + t.Fatalf("omitted.Width() = %v, want preferred width 35", got) + } + if got := autoA.Width(); got != 102.5 { + t.Fatalf("autoA.Width() = %v, want 102.5", got) + } + if got := autoB.Width(); got != 102.5 { + t.Fatalf("autoB.Width() = %v, want 102.5", got) + } +} + +func TestLayoutTable_AutoWidthPreservesPercentColumns(t *testing.T) { + c := testTableContainer(400, 100, 4) + + percent := addTableTestWidget(t, c, 30, 20) + percent.SetWidthPct(25) + fixed := addTableTestWidget(t, c, 30, 20) + fixed.SetWidth(50) + omitted := addTableTestWidget(t, c, 40, 20) + auto := addTableTestWidget(t, c, 50, 20) + auto.SetWidthAuto() + + LayoutTable(c, c.LayoutStyle(), &labelTestWriter{t: t}) + + if got := percent.Width(); got != 100 { + t.Fatalf("percent.Width() = %v, want 100", got) + } + if got := fixed.Width(); got != 50 { + t.Fatalf("fixed.Width() = %v, want 50", got) + } + if got := omitted.Width(); got != 40 { + t.Fatalf("omitted.Width() = %v, want preferred width 40", got) + } + if got := auto.Width(); got != 210 { + t.Fatalf("auto.Width() = %v, want remaining width 210", got) + } +} + +func TestLayoutTable_LaterSingleColumnAutoCellMarksColumnAuto(t *testing.T) { + c := testTableContainer(220, 100, 2) + + omittedA := addTableTestWidget(t, c, 40, 20) + omittedB := addTableTestWidget(t, c, 30, 20) + laterAutoA := addTableTestWidget(t, c, 45, 20) + laterAutoA.SetWidthAuto() + laterAutoB := addTableTestWidget(t, c, 35, 20) + + LayoutTable(c, c.LayoutStyle(), &labelTestWriter{t: t}) + + if got := omittedB.Width(); got != 35 { + t.Fatalf("omittedB.Width() = %v, want preferred width 35", got) + } + if got := omittedA.Width(); got != 185 { + t.Fatalf("omittedA.Width() = %v, want auto column surplus width 185", got) + } + if got := laterAutoA.Width(); got != 185 { + t.Fatalf("laterAutoA.Width() = %v, want auto column surplus width 185", got) + } + if got := laterAutoB.Width(); got != 35 { + t.Fatalf("laterAutoB.Width() = %v, want omitted column preferred width 35", got) + } +} + +func TestLayoutTable_OmittedWidthsKeepLegacyEqualShareWithoutAutoColumns(t *testing.T) { + c := testTableContainer(300, 80, 3) + + a := addTableTestWidget(t, c, 20, 20) + b := addTableTestWidget(t, c, 60, 20) + d := addTableTestWidget(t, c, 100, 20) + + LayoutTable(c, c.LayoutStyle(), &labelTestWriter{t: t}) + + for i, w := range []*positionedTestWidget{a, b, d} { + if got := w.Width(); got != 100 { + t.Fatalf("widget %d Width() = %v, want legacy equal share 100", i, got) + } + } +} + +func TestLayoutTable_AutoWidthPreservesOmittedPreferredWidthWhenAutosCanShrink(t *testing.T) { + c := testTableContainer(120, 80, 3) + + omitted := addTableTestWidget(t, c, 100, 20) + autoA := addTableTestWidget(t, c, 80, 20) + autoA.SetWidthAuto() + autoB := addTableTestWidget(t, c, 80, 20) + autoB.SetWidthAuto() + + LayoutTable(c, c.LayoutStyle(), &labelTestWriter{t: t}) + + if got := omitted.Width(); got != 100 { + t.Fatalf("omitted.Width() = %v, want preferred width 100", got) + } + if got := autoA.Width(); got != 10 { + t.Fatalf("autoA.Width() = %v, want remaining auto share 10", got) + } + if got := autoB.Width(); got != 10 { + t.Fatalf("autoB.Width() = %v, want remaining auto share 10", got) + } +} + +func TestLayoutTable_AutoWidthFallsBackToEqualShareWhenOmittedPreferredCannotFit(t *testing.T) { + c := testTableContainer(90, 80, 3) + + omitted := addTableTestWidget(t, c, 100, 20) + autoA := addTableTestWidget(t, c, 80, 20) + autoA.SetWidthAuto() + autoB := addTableTestWidget(t, c, 80, 20) + autoB.SetWidthAuto() + + LayoutTable(c, c.LayoutStyle(), &labelTestWriter{t: t}) + + for i, w := range []*positionedTestWidget{omitted, autoA, autoB} { + if got := w.Width(); got != 30 { + t.Fatalf("widget %d Width() = %v, want impossible-case equal share 30", i, got) + } + } +} + +func TestLayoutTable_ColspanDoesNotCreateAutoColumnConstraint(t *testing.T) { + c := testTableContainer(240, 100, 2) + + spanning := addTableTestWidget(t, c, 200, 20) + spanning.SetWidthAuto() + spanning.SetAttrs(map[string]string{"colspan": "2"}) + + a := addTableTestWidget(t, c, 30, 20) + b := addTableTestWidget(t, c, 50, 20) + + LayoutTable(c, c.LayoutStyle(), &labelTestWriter{t: t}) + + if got := a.Width(); got != 120 { + t.Fatalf("a.Width() = %v, want legacy equal share 120", got) + } + if got := b.Width(); got != 120 { + t.Fatalf("b.Width() = %v, want legacy equal share 120", got) + } + if got := spanning.Width(); got != 240 { + t.Fatalf("spanning.Width() = %v, want full two-column span 240", got) + } +} + +func TestLayoutTable_AutoHeightRowsShareSurplus(t *testing.T) { + c := testTableContainer(200, 150, 2) + + fixedA := addTableTestWidget(t, c, 30, 20) + fixedA.SetHeight(20) + fixedB := addTableTestWidget(t, c, 30, 20) + fixedB.SetHeight(20) + + omittedA := addTableTestWidget(t, c, 30, 30) + omittedB := addTableTestWidget(t, c, 30, 30) + + autoA := addTableTestWidget(t, c, 30, 10) + autoA.SetHeightAuto() + autoB := addTableTestWidget(t, c, 30, 10) + autoB.SetHeightAuto() + + LayoutTable(c, c.LayoutStyle(), &labelTestWriter{t: t}) + + if got := omittedA.Height(); got != 30 { + t.Fatalf("omittedA.Height() = %v, want preferred height 30", got) + } + if got := omittedB.Height(); got != 30 { + t.Fatalf("omittedB.Height() = %v, want preferred height 30", got) + } + if got := autoA.Height(); got != 100 { + t.Fatalf("autoA.Height() = %v, want 100", got) + } + if got := autoB.Height(); got != 100 { + t.Fatalf("autoB.Height() = %v, want 100", got) + } +} + +func TestLayoutTable_AutoHeightRowsPreservePercentRows(t *testing.T) { + c := testTableContainer(200, 200, 2) + + percentA := addTableTestWidget(t, c, 30, 20) + percentA.SetHeightPct(25) + percentB := addTableTestWidget(t, c, 30, 20) + percentB.SetHeightPct(25) + + omittedA := addTableTestWidget(t, c, 30, 30) + omittedB := addTableTestWidget(t, c, 30, 30) + + autoA := addTableTestWidget(t, c, 30, 10) + autoA.SetHeightAuto() + autoB := addTableTestWidget(t, c, 30, 10) + autoB.SetHeightAuto() + + LayoutTable(c, c.LayoutStyle(), &labelTestWriter{t: t}) + + if got := percentA.Height(); got != 50 { + t.Fatalf("percentA.Height() = %v, want 50", got) + } + if got := percentB.Height(); got != 50 { + t.Fatalf("percentB.Height() = %v, want 50", got) + } + if got := omittedA.Height(); got != 30 { + t.Fatalf("omittedA.Height() = %v, want preferred height 30", got) + } + if got := omittedB.Height(); got != 30 { + t.Fatalf("omittedB.Height() = %v, want preferred height 30", got) + } + if got := autoA.Height(); got != 120 { + t.Fatalf("autoA.Height() = %v, want remaining auto height 120", got) + } + if got := autoB.Height(); got != 120 { + t.Fatalf("autoB.Height() = %v, want remaining auto height 120", got) + } +} + +func TestLayoutTable_NaturalHeightLeavesAutoRowsAtPreferredHeight(t *testing.T) { + c := testTableContainer(200, 0, 1) + + auto := addTableTestWidget(t, c, 30, 25) + auto.SetHeightAuto() + omitted := addTableTestWidget(t, c, 30, 35) + + LayoutTable(c, c.LayoutStyle(), &labelTestWriter{t: t}) + + if got := auto.Height(); got != 25 { + t.Fatalf("auto.Height() = %v, want preferred height 25", got) + } + if got := omitted.Height(); got != 35 { + t.Fatalf("omitted.Height() = %v, want preferred height 35", got) + } + if got := c.Height(); got != 60 { + t.Fatalf("container.Height() = %v, want natural height 60", got) + } +} diff --git a/ltml/ltml_test.go b/ltml/ltml_test.go index 700e141..0a960e4 100644 --- a/ltml/ltml_test.go +++ b/ltml/ltml_test.go @@ -250,6 +250,9 @@ func TestSamples(t *testing.T) { "test_056_hbox_auto_width", "test_057_vbox_auto_height", "test_058_vbox_auto_height_overflow", + "test_059_table_auto_width", + "test_060_table_auto_height", + "test_061_table_auto_split", } for _, sample := range samples { diff --git a/ltml/samples/test_059_table_auto_width.ltml b/ltml/samples/test_059_table_auto_width.ltml new file mode 100644 index 0000000..ac95414 --- /dev/null +++ b/ltml/samples/test_059_table_auto_width.ltml @@ -0,0 +1,85 @@ + + + + + + + + + + + + + +

+ Fixed and percent columns keep their authored sizes. In a roomy table, + omitted columns keep preferred width while width="auto" columns split + surplus. In a tight table, auto columns shrink first so omitted columns + can keep preferred width. +

+ +
Roomy table
+ + + + + + +
+ +
Tight table fallback
+ + + + + + +
+
+
diff --git a/ltml/samples/test_059_table_auto_width.pdf b/ltml/samples/test_059_table_auto_width.pdf new file mode 100644 index 0000000000000000000000000000000000000000..9981403bac7c41402a8211c143ab7195e16fc57f GIT binary patch literal 36711 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 + + + + + + + + + + + +

+ Fixed-height tables preserve fixed and percent rows, then share true + surplus height only with rows containing height="auto" cells. + Natural-height tables leave auto rows at preferred height. +

+ +
Roomy fixed-height table
+ + + + + + + + + +
+ +
Natural-height table
+ + + + + +
+
+ diff --git a/ltml/samples/test_060_table_auto_height.pdf b/ltml/samples/test_060_table_auto_height.pdf new file mode 100644 index 0000000000000000000000000000000000000000..3a8cc9c69007e034bfa4ca3ef91765f35a9fcc2f GIT binary patch literal 37123 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 literal 0 HcmV?d00001 diff --git a/ltml/samples/test_061_table_auto_split.ltml b/ltml/samples/test_061_table_auto_split.ltml new file mode 100644 index 0000000..2895fe1 --- /dev/null +++ b/ltml/samples/test_061_table_auto_split.ltml @@ -0,0 +1,76 @@ + + + + + + + + + + + +

+ This direct page-child table is a height="auto" child of the page vbox. + Each split fragment receives the remaining page height, then shares + surplus only with the height="auto" table rows present on that fragment. +

+ + +

Repeating table header

+

row 1 A

+

row 1 B

+

row 2 auto A

+

row 2 auto B

+

row 3 A

+

row 3 B

+

row 4 A

+

row 4 B

+

row 5 auto A

+

row 5 auto B

+

row 6 A

+

row 6 B

+

row 7 A

+

row 7 B

+

row 8 auto A

+

row 8 auto B

+

row 9 A

+

row 9 B

+

row 10 A

+

row 10 B

+
+
+
diff --git a/ltml/samples/test_061_table_auto_split.pdf b/ltml/samples/test_061_table_auto_split.pdf new file mode 100644 index 0000000000000000000000000000000000000000..b00754fc2420a83311517241b467e03694dd2708 GIT binary patch literal 43608 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 z
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 literal 0 HcmV?d00001 diff --git a/ltml/std_container.go b/ltml/std_container.go index 4ad0e38..d0df6e0 100644 --- a/ltml/std_container.go +++ b/ltml/std_container.go @@ -338,6 +338,14 @@ func (c *StdContainer) SplitForHeight(avail float64, w Writer) (*SplitResult, er return nil, err } bodyCount := metrics.bodyEnd - metrics.bodyStart + if c.tableFragmentHeight(metrics, metrics.bodyStart, metrics.bodyEnd) <= avail { + rows := append([]int{}, metrics.headerRows...) + for r := metrics.bodyStart; r < metrics.bodyEnd; r++ { + rows = append(rows, r) + } + rows = append(rows, metrics.footerRows...) + return &SplitResult{Head: c.cloneTableFragment(metrics, rows), Tail: nil}, nil + } if bodyCount < 2 { return nil, nil } @@ -430,13 +438,7 @@ func (c *StdContainer) tableSplitMetrics(w Writer) (*tableSplitMetrics, error) { return nil, errTableSplitUnsupportedRowSpan } } - widths := detectWidths(grid, w) - percents, others := widths.Partition(func(w *SpecifiedSize) bool { return w.How == Percent }) - specified, others := others.Partition(func(w *SpecifiedSize) bool { return w.How == Specified }) - widthAvail := ContentWidth(c) - widthAvail = allocateSpecifiedWidths(widthAvail, specified, c.LayoutStyle()) - widthAvail = allocatePercentWidths(widthAvail, percents, c.LayoutStyle()) - _ = allocateOtherWidths(widthAvail, others, c.LayoutStyle()) + widths := planTableColumnWidths(grid, c, c.LayoutStyle(), w).resolvedSizes() rowHeights := make([]float64, grid.Rows()) for r := 0; r < grid.Rows(); r++ { @@ -446,14 +448,10 @@ func (c *StdContainer) tableSplitMetrics(w Writer) (*tableSplitMetrics, error) { if widget == nil { continue } - if widths[col].Size <= 0 { + if widths[col] <= 0 { continue } - width := 0.0 - for i := 0; i < widget.ColSpan(); i++ { - width += widths[col+i].Size - } - widget.ResolveWidth(width + float64(widget.ColSpan()-1)*c.LayoutStyle().HPadding()) + widget.ResolveWidth(tableCellWidth(widths, col, widget.ColSpan(), c.LayoutStyle().HPadding())) height := widget.Height() if !widgetHeightSpecified(widget) { height = widget.PreferredHeight(w) From 4252f8427e857fe5332aef96a94fe9e919f53545 Mon Sep 17 00:00:00 2001 From: Brent Rowland Date: Sat, 9 May 2026 10:47:19 -0700 Subject: [PATCH 5/5] Fix radial sample test path --- ltml/radial_layout_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ltml/radial_layout_test.go b/ltml/radial_layout_test.go index cb6d678..71a2215 100644 --- a/ltml/radial_layout_test.go +++ b/ltml/radial_layout_test.go @@ -1008,7 +1008,7 @@ func TestStdSector_WithParagraphChild_DoesNotDefaultToTangentRotation(t *testing } func TestRadialSample_ImplicitParagraphsKeepLegacyPlacement(t *testing.T) { - doc, err := ParseFile("/Users/brent/src/leadtype/ltml/samples/test_038_radial_layout.ltml") + doc, err := ParseFile(sampleFile("test_038_radial_layout.ltml")) if err != nil { t.Fatal(err) }