1// Package layout computes logical-pixel boxes and an ordered, serializable paint list.
2// Input nodes and CSS styles are borrowed for one synchronous Layout call.
3package layout
4
5import (
6 "errors"
7 "image"
8 "math"
9
10 "github.com/bnema/nefergui/internal/css"
11 "github.com/bnema/nefergui/internal/text"
12 "github.com/go-text/typesetting/di"
13)
14
15// Kind selects intrinsic and container behavior. CSS display:none suppresses all kinds;
16// display:flex takes precedence over Kind, and row/column flex defaults are overridable.
17type Kind string
18
19const (
20 Box Kind = "box"
21 Row Kind = "row"
22 Column Kind = "column"
23 Stack Kind = "stack"
24 Scroll Kind = "scroll"
25 Text Kind = "text"
26 Image Kind = "image"
27)
28const limit = 1 << 24
29
30// Rect uses logical pixels, including fractional positions.
31type Rect struct{ X, Y, W, H float64 }
32
33func (r Rect) Contains(x, y float64) bool { return x >= r.X && y >= r.Y && x < r.X+r.W && y < r.Y+r.H }
34func (r Rect) intersect(q Rect) Rect {
35 x := math.Max(r.X, q.X)
36 y := math.Max(r.Y, q.Y)
37 return Rect{x, y, math.Max(0, math.Min(r.X+r.W, q.X+q.W)-x), math.Max(0, math.Min(r.Y+r.H, q.Y+q.H)-y)}
38}
39func safe(v float64) float64 {
40 if math.IsNaN(v) || v < 0 {
41 return 0
42 }
43 if math.IsInf(v, 0) || v > limit {
44 return limit
45 }
46 return v
47}
48func coord(v float64) float64 {
49 if math.IsNaN(v) {
50 return 0
51 }
52 if math.IsInf(v, 1) {
53 return limit
54 }
55 if math.IsInf(v, -1) {
56 return -limit
57 }
58 return math.Max(-limit, math.Min(limit, v))
59}
60func length(l css.Length, basis float64) (float64, bool) {
61 switch l.Unit {
62 case "px":
63 return coord(l.Value), true
64 case "%":
65 return coord(safe(basis) * l.Value / 100), true
66 }
67 return 0, false
68}
69func positive(l css.Length, basis float64) float64 { v, _ := length(l, basis); return safe(v) }
70func edges(s css.LengthSides, basis float64) Edges {
71 return Edges{coordValue(s.Top, basis), coordValue(s.Right, basis), coordValue(s.Bottom, basis), coordValue(s.Left, basis)}
72}
73func coordValue(l css.Length, basis float64) float64 { v, _ := length(l, basis); return v }
74
75// Edges are in top/right/bottom/left order.
76type Edges struct{ Top, Right, Bottom, Left float64 }
77
78func (e Edges) horizontal() float64 { return e.Left + e.Right }
79func (e Edges) vertical() float64 { return e.Top + e.Bottom }
80func nonnegative(e Edges) Edges {
81 return Edges{safe(e.Top), safe(e.Right), safe(e.Bottom), safe(e.Left)}
82}
83
84// Measure receives the available content width (zero means unbounded), and must
85// return finite logical dimensions and optionally shaped lines. It may return an error.
86// TextEngine, when supplied, is used for Text nodes without an explicit callback.
87type Measure func(width float64) (Size, []text.Line, error)
88type Size struct{ W, H float64 }
89
90// Node has no parent pointers or renderer resources. ID identifies hit targets and
91// display commands; callers must supply stable, unique IDs if they need identity.
92// ImageSize is the decoded image's intrinsic size; image ownership remains with caller.
93// Image is borrowed through painting. Treat it as immutable while passed to Image:
94// pass a new image value to change pixels. Renderer cache identity requires a
95// comparable image.Image interface value (for example, *image.RGBA).
96type Node struct {
97 ID string
98 Kind Kind
99 Style *css.Style
100 Children []*Node
101 Content string
102 ImageSize Size
103 Image image.Image `json:"-"`
104 Measure Measure `json:"-"`
105 ScrollX, ScrollY float64
106 NoWrap bool // measure text at unbounded width; useful for single-line editors
107 EditorScroll bool // clip and clamp own text on both axes, without child scrolling
108}
109
110// Result is a detached layout snapshot. Style is deliberately not retained.
111type Result struct {
112 ID string
113 Kind Kind
114 Border, Content, Clip Rect
115 Margin, Padding, BorderWidths Edges
116 ContentSize Size
117 ScrollX, ScrollY float64
118 Lines []text.Line `json:"-"`
119 Children []*Result
120 Clipped bool
121}
122
123// Command is a paint operation. Text line ranges refer to rune offsets
124// in Text; image pixels are borrowed and excluded from serialized display lists.
125type Command struct {
126 Op string
127 ID string
128 Rect Rect
129 Color css.Color
130 Opacity float64
131 Widths Edges
132 Colors css.ColorSides
133 Radii [4]float64
134 Shadow *css.Shadow `json:",omitempty"`
135 Text string `json:",omitempty"`
136 Runs []Run `json:",omitempty"`
137 Image image.Image `json:"-"`
138}
139type Run struct {
140 Start, End int
141 X, Y, Advance float64
142 // Face is borrowed for in-process rasterization. FaceID and Size preserve
143 // the shaping identity and logical pixel size in serialized display lists.
144 Face *text.Face `json:"-"`
145 FaceID string
146 Size float64
147 Glyphs []Glyph
148}
149type Glyph struct {
150 ID uint32
151 X, Y, Advance float64
152 Cluster int
153}
154
155// Output contains a JSON-safe tree and paint list in back-to-front command order.
156type Output struct {
157 Tree *Result
158 Display []Command
159}
160
161// Options provide logical viewport size and an optional text shaping engine.
162type Options struct {
163 Width, Height float64
164 TextEngine *text.Engine
165}
166
167var ErrDepth = errors.New("layout: tree depth exceeds 256")
168
169// Layout does not mutate inputs. The caller must not create cycles in the node tree.
170func Layout(root *Node, options Options) (Output, error) {
171 if root == nil {
172 return Output{}, nil
173 }
174 ctx := context{options: options}
175 viewport := Rect{0, 0, safe(options.Width), safe(options.Height)}
176 tree, err := ctx.place(root, viewport.X, viewport.Y, viewport.W, viewport.H, nil, nil, 0)
177 if err != nil {
178 return Output{}, err
179 }
180 out := Output{Tree: tree}
181 out.Display = ctx.display(root, tree)
182 return out, nil
183}
184
185// display paints the placed tree into a list sized once from the tree.
186func (c context) display(root *Node, tree *Result) []Command {
187 n := countCommands(root, tree)
188 if n.commands == 0 {
189 return nil
190 }
191 out := make([]Command, 0, n.commands)
192 c.shadows, c.runs, c.glyphs = newSlab[css.Shadow](n.shadows), newSlab[Run](n.runs), newSlab[Glyph](n.glyphs)
193 c.paint(root, tree, &out)
194 return out
195}
196
197type context struct {
198 options Options
199 // Painter-owned display storage, sized by countCommands; nil is valid.
200 shadows *slab[css.Shadow]
201 runs *slab[Run]
202 glyphs *slab[Glyph]
203}
204
205func style(n *Node) css.Style {
206 if n.Style != nil {
207 return *n.Style
208 }
209 return css.Style{Display: css.KeywordBlock, OverflowX: css.KeywordVisible, OverflowY: css.KeywordVisible, FlexShrink: 1}
210}
211func bounded(v float64, minL, maxL css.Length, basis float64) float64 {
212 v = safe(v)
213 if min, ok := length(minL, basis); ok {
214 v = math.Max(v, safe(min))
215 }
216 if max, ok := length(maxL, basis); ok {
217 v = math.Min(v, safe(max))
218 }
219 return safe(v)
220}
221
222// boxBound applies constraints to the CSS sizing space, not to the outer box.
223func boxBound(border float64, minL, maxL css.Length, basis, insets float64, borderBox bool) float64 {
224 if borderBox {
225 return math.Max(insets, bounded(border, minL, maxL, basis))
226 }
227 return safe(bounded(safe(border-insets), minL, maxL, basis) + insets)
228}
229func (c context) intrinsic(n *Node, w float64) (Size, []text.Line, error) {
230 return c.intrinsicDepth(n, w, 0)
231}
232
233func (c context) intrinsicDepth(n *Node, w float64, depth int) (Size, []text.Line, error) {
234 if depth > 256 {
235 return Size{}, nil, ErrDepth
236 }
237 if n.Measure != nil {
238 s, lines, err := n.Measure(w)
239 if err != nil {
240 return Size{}, nil, err
241 }
242 for _, line := range lines {
243 if math.IsNaN(line.Width) || math.IsInf(line.Width, 0) || math.IsNaN(line.Height) || math.IsInf(line.Height, 0) {
244 return Size{}, nil, errors.New("layout: invalid measured text")
245 }
246 }
247 return Size{safe(s.W), safe(s.H)}, lines, nil
248 }
249 if n.Kind == Image {
250 return Size{safe(n.ImageSize.W), safe(n.ImageSize.H)}, nil, nil
251 }
252 if n.Kind == Text && c.options.TextEngine != nil {
253 st := style(n)
254 req := text.Request{Families: st.FontFamily, Weight: float32(st.FontWeight), Italic: st.FontStyle == css.KeywordItalic || st.FontStyle == css.KeywordOblique, Size: safe(st.FontSize.Value), LetterSpacing: coordValue(st.LetterSpacing, 0)}
255 if req.Size == 0 {
256 req.Size = 16
257 }
258 content := n.Content
259 if content == "" && n.EditorScroll {
260 // Preserve one font-metric line for empty editors, caret geometry
261 // and IME composition without painting a visible glyph.
262 content = " "
263 }
264 l, err := c.options.TextEngine.Measure(content, req, w)
265 if err != nil {
266 return Size{}, nil, err
267 }
268 lineHeight := l.LineHeight
269 switch st.LineHeightKind {
270 case css.KeywordNumber:
271 lineHeight = safe(req.Size * st.LineHeightNumber)
272 case css.KeywordNormal:
273 default:
274 if st.LineHeight.Unit != "auto" && st.LineHeight.Unit != "" {
275 lineHeight = positive(st.LineHeight, req.Size)
276 }
277 }
278 // Keep the glyphs' shaped positions; adjust baseline and line origins, not outlines.
279 if lineHeight != l.LineHeight && len(l.Lines) > 0 {
280 l = l.Clone() // Measure results are shared with the engine cache
281 for i := range l.Lines {
282 line := &l.Lines[i]
283 dy := float64(i)*(lineHeight-l.LineHeight) + (lineHeight-l.LineHeight)/2
284 line.Baseline += dy
285 line.Height = lineHeight
286 for j := range line.Runs {
287 run := &line.Runs[j]
288 run.Y += dy
289 for k := range run.Glyphs {
290 run.Glyphs[k].Y += dy
291 }
292 }
293 }
294 l.Height = float64(len(l.Lines)) * lineHeight
295 }
296 return Size{safe(l.Width), safe(l.Height)}, l.Lines, nil
297 }
298 if depth == 0 {
299 return Size{}, nil, nil
300 }
301 // Containers contribute their children to auto flex sizing. Measuring them
302 // as zero compresses nested columns and makes empty editors collapse.
303 st := style(n)
304 row := st.Display == css.KeywordFlex && st.FlexDirection != css.KeywordColumn
305 gap := positive(st.RowGap, w)
306 if row {
307 gap = positive(st.ColumnGap, w)
308 }
309 var size Size
310 count := 0
311 for _, child := range n.Children {
312 if child == nil || style(child).Display == css.KeywordNone {
313 continue
314 }
315 cs := style(child)
316 inW, inH := flexInsets(cs, w)
317 measured, _, err := c.intrinsicDepth(child, safe(w-inW), depth+1)
318 if err != nil {
319 return Size{}, nil, err
320 }
321 cw, ch := measured.W+inW, measured.H+inH
322 if value, ok := length(cs.Width, w); ok && cs.Width.Unit != "%" {
323 cw = value
324 if cs.BoxSizing != css.KeywordBorderBox {
325 cw += inW
326 }
327 }
328 // Percent heights need a definite containing height, unavailable here.
329 if cs.Height.Unit != "%" {
330 if value, ok := length(cs.Height, 0); ok {
331 ch = value
332 if cs.BoxSizing != css.KeywordBorderBox {
333 ch += inH
334 }
335 }
336 }
337 cw = boxBound(cw, cs.MinWidth, cs.MaxWidth, w, inW, cs.BoxSizing == css.KeywordBorderBox)
338 ch = boxBound(ch, cs.MinHeight, cs.MaxHeight, 0, inH, cs.BoxSizing == css.KeywordBorderBox)
339 // Natural measurement is an unwrapped envelope; actual placement
340 // distributes flex-basis and wraps against the definite container size.
341 margin := edges(cs.Margin, w)
342 cw += margin.horizontal()
343 ch += margin.vertical()
344 if row {
345 if count > 0 {
346 size.W += gap
347 }
348 size.W += cw
349 size.H = math.Max(size.H, ch)
350 } else if n.Kind == Stack {
351 size.W = math.Max(size.W, cw)
352 size.H = math.Max(size.H, ch)
353 } else {
354 if count > 0 && st.Display == css.KeywordFlex {
355 size.H += gap
356 }
357 size.W = math.Max(size.W, cw)
358 size.H += ch
359 }
360 count++
361 }
362 return size, nil, nil
363}
364func (c context) place(n *Node, x, y, availableW, availableH float64, forcedW, forcedH *float64, depth int) (*Result, error) {
365 if depth > 256 {
366 return nil, ErrDepth
367 }
368 if n == nil {
369 return nil, nil
370 }
371 s := style(n)
372 if s.Display == css.KeywordNone {
373 return nil, nil
374 }
375 availableW = safe(availableW)
376 availableH = safe(availableH)
377 margin := edges(s.Margin, availableW)
378 padding := nonnegative(edges(s.Padding, availableW))
379 border := nonnegative(edges(s.BorderWidth, availableW))
380 if s.BorderStyle.Top != css.KeywordSolid {
381 border.Top = 0
382 }
383 if s.BorderStyle.Right != css.KeywordSolid {
384 border.Right = 0
385 }
386 if s.BorderStyle.Bottom != css.KeywordSolid {
387 border.Bottom = 0
388 }
389 if s.BorderStyle.Left != css.KeywordSolid {
390 border.Left = 0
391 }
392 outerW := safe(availableW - margin.horizontal())
393 insetW := padding.horizontal() + border.horizontal()
394 insetH := padding.vertical() + border.vertical()
395 w, explicitW := length(s.Width, availableW)
396 h, explicitH := length(s.Height, availableH)
397 borderBox := s.BoxSizing == css.KeywordBorderBox
398 // Forced flex lengths are border-box; authored dimensions and constraints use box-sizing.
399 if forcedW != nil {
400 w, explicitW = *forcedW, true
401 } else if explicitW {
402 w = bounded(w, s.MinWidth, s.MaxWidth, availableW)
403 if !borderBox {
404 w += insetW
405 }
406 }
407 if forcedH != nil {
408 h, explicitH = *forcedH, true
409 } else if explicitH {
410 h = bounded(h, s.MinHeight, s.MaxHeight, availableH)
411 if !borderBox {
412 h += insetH
413 }
414 }
415 if !explicitW {
416 w = outerW
417 if n.Kind == Image {
418 w = 0
419 }
420 }
421 if !explicitW && n.Kind != Image {
422 w = boxBound(w, s.MinWidth, s.MaxWidth, availableW, insetW, borderBox)
423 }
424 w = safe(math.Max(w, insetW))
425 contentW := safe(w - insetW)
426 measureW := contentW
427 if n.Kind == Text && n.NoWrap {
428 measureW = 0
429 }
430 measured, lines, err := c.intrinsic(n, measureW)
431 if err != nil {
432 return nil, err
433 }
434 if !explicitW && n.Kind == Image {
435 w = boxBound(measured.W+insetW, s.MinWidth, s.MaxWidth, availableW, insetW, borderBox)
436 contentW = safe(w - insetW)
437 measured, lines, err = c.intrinsic(n, contentW)
438 if err != nil {
439 return nil, err
440 }
441 }
442 if !explicitH {
443 h = measured.H + insetH
444 if n.Kind == Image && measured.W > 0 && explicitW {
445 h = measured.H*contentW/measured.W + insetH
446 }
447 }
448 if n.Kind == Image && !explicitW && explicitH && measured.H > 0 {
449 w = boxBound(measured.W*safe(h-insetH)/measured.H+insetW, s.MinWidth, s.MaxWidth, availableW, insetW, borderBox)
450 contentW = safe(w - insetW)
451 }
452 h = safe(math.Max(h, insetH))
453 if !explicitH {
454 h = boxBound(h, s.MinHeight, s.MaxHeight, availableH, insetH, borderBox)
455 }
456 r := &Result{ID: n.ID, Kind: n.Kind, Border: Rect{coord(x + margin.Left), coord(y + margin.Top), w, h}, Margin: margin, Padding: padding, BorderWidths: border, Lines: lines}
457 r.Content = Rect{coord(r.Border.X + border.Left + padding.Left), coord(r.Border.Y + border.Top + padding.Top), contentW, safe(h - insetH)}
458 r.ContentSize = Size{measured.W, measured.H}
459 // Block and stack auto heights depend on their children's envelopes.
460 flex := s.Display == css.KeywordFlex
461 if flex {
462 // An auto-height flex container needs its children's natural height
463 // before distributing space. Otherwise every child is shrunk into zero.
464 // This repeats subtree measurement for nested auto-height containers;
465 // a node/width map cache cost more time and memory in our benchmarks.
466 if !explicitH {
467 natural, _, measureErr := c.intrinsicDepth(n, contentW, 1)
468 if measureErr != nil {
469 return nil, measureErr
470 }
471 r.Border.H = boxBound(math.Max(natural.H+insetH, h), s.MinHeight, s.MaxHeight, availableH, insetH, borderBox)
472 r.Content.H = safe(r.Border.H - insetH)
473 }
474 err = c.flex(n, r, s, depth)
475 } else {
476 err = c.children(n, r, s, depth)
477 }
478 if err != nil {
479 return nil, err
480 }
481 if !explicitH {
482 r.Border.H = boxBound(math.Max(safe(h-insetH), r.ContentSize.H)+insetH, s.MinHeight, s.MaxHeight, availableH, insetH, borderBox)
483 r.Content.H = safe(r.Border.H - insetH)
484 }
485 clipX := overflowClips(s.OverflowX) || n.Kind == Scroll || n.EditorScroll
486 clipY := overflowClips(s.OverflowY) || n.Kind == Scroll || n.EditorScroll
487 if clipX || clipY {
488 r.Clipped = true
489 r.Clip = r.Content
490 if !clipX {
491 r.Clip.X = -limit
492 r.Clip.W = 2 * limit
493 }
494 if !clipY {
495 r.Clip.Y = -limit
496 r.Clip.H = 2 * limit
497 }
498 if clipX && (s.OverflowX == css.KeywordScroll || s.OverflowX == css.KeywordAuto || n.Kind == Scroll || n.EditorScroll) {
499 r.ScrollX = math.Min(safe(n.ScrollX), safe(r.ContentSize.W-r.Content.W))
500 }
501 if clipY && (s.OverflowY == css.KeywordScroll || s.OverflowY == css.KeywordAuto || n.Kind == Scroll || n.EditorScroll) {
502 r.ScrollY = math.Min(safe(n.ScrollY), safe(r.ContentSize.H-r.Content.H))
503 }
504 for _, child := range r.Children {
505 shift(child, -r.ScrollX, -r.ScrollY)
506 }
507 }
508 return r, nil
509}
510func overflowClips(k css.Keyword) bool {
511 return k == css.KeywordHidden || k == css.KeywordScroll || k == css.KeywordAuto
512}
513func shift(r *Result, dx, dy float64) {
514 r.Border.X = coord(r.Border.X + dx)
515 r.Border.Y = coord(r.Border.Y + dy)
516 r.Content.X = coord(r.Content.X + dx)
517 r.Content.Y = coord(r.Content.Y + dy)
518 r.Clip.X = coord(r.Clip.X + dx)
519 r.Clip.Y = coord(r.Clip.Y + dy)
520 for _, child := range r.Children {
521 shift(child, dx, dy)
522 }
523}
524func envelope(r *Result) (float64, float64) {
525 return safe(r.Border.W + r.Margin.horizontal()), safe(r.Border.H + r.Margin.vertical())
526}
527func (c context) children(n *Node, r *Result, s css.Style, depth int) error {
528 y := r.Content.Y
529 for _, ch := range n.Children {
530 if ch == nil {
531 continue
532 }
533 child, err := c.place(ch, r.Content.X, y, r.Content.W, r.Content.H, nil, nil, depth+1)
534 if err != nil {
535 return err
536 }
537 if child == nil {
538 continue
539 }
540 cw, chh := envelope(child)
541 r.Children = append(r.Children, child)
542 r.ContentSize.W = math.Max(r.ContentSize.W, cw)
543 if n.Kind == Stack {
544 r.ContentSize.H = math.Max(r.ContentSize.H, chh)
545 } else {
546 y = coord(y + chh)
547 r.ContentSize.H = math.Max(r.ContentSize.H, y-r.Content.Y)
548 }
549 }
550 return nil
551}
552
553type flexItem struct {
554 node *Node
555 base, size, cross, grow, shrink, marginMain, marginCross float64 // sizes are border boxes
556 min, max float64
557 frozen bool
558}
559
560// flexInsets computes effective border-box insets, using the same border-style
561// treatment as place. Percentage padding is relative to the containing width.
562func flexInsets(s css.Style, basis float64) (float64, float64) {
563 p := nonnegative(edges(s.Padding, basis))
564 b := nonnegative(edges(s.BorderWidth, basis))
565 if s.BorderStyle.Top != css.KeywordSolid {
566 b.Top = 0
567 }
568 if s.BorderStyle.Right != css.KeywordSolid {
569 b.Right = 0
570 }
571 if s.BorderStyle.Bottom != css.KeywordSolid {
572 b.Bottom = 0
573 }
574 if s.BorderStyle.Left != css.KeywordSolid {
575 b.Left = 0
576 }
577 return p.horizontal() + b.horizontal(), p.vertical() + b.vertical()
578}
579func flexBounds(s css.Style, main bool, basis, inset float64) (float64, float64) {
580 minL, maxL := s.MinWidth, s.MaxWidth
581 if !main {
582 minL, maxL = s.MinHeight, s.MaxHeight
583 }
584 low, high := inset, float64(limit)
585 if v, ok := length(minL, basis); ok {
586 if s.BoxSizing != css.KeywordBorderBox {
587 low = math.Max(inset, safe(v)+inset)
588 } else {
589 low = math.Max(inset, safe(v))
590 }
591 }
592 if v, ok := length(maxL, basis); ok {
593 high = safe(v)
594 if s.BoxSizing != css.KeywordBorderBox {
595 high += inset
596 }
597 high = math.Max(high, inset)
598 }
599 return low, math.Max(low, high)
600}
601
602// resolveFlex freezes violating items, then redistributes the remaining free
603// space among unfrozen items. Shrink weights always use the original base.
604func resolveFlex(items []flexItem, available float64) {
605 total := 0.0
606 for i := range items {
607 it := &items[i]
608 it.size = it.base
609 it.frozen = false
610 total += it.base + it.marginMain
611 }
612 grow := available > total
613 // Each pass freezes at least one violator, or all remaining items.
614 for range items {
615 free, weight, factors := available, 0.0, 0.0
616 for i := range items {
617 it := &items[i]
618 free -= it.marginMain
619 if it.frozen {
620 free -= it.size
621 continue
622 }
623 free -= it.base
624 factor := it.grow
625 if !grow {
626 factor = it.shrink * it.base
627 }
628 weight += factor
629 if grow {
630 factors += it.grow
631 }
632 }
633 // CSS resolve-flexible-lengths limits growth when total grow factors < 1.
634 if grow && factors < 1 {
635 free *= factors
636 }
637 violation := 0.0
638 for i := range items {
639 it := &items[i]
640 if it.frozen {
641 continue
642 }
643 factor := it.grow
644 if !grow {
645 factor = it.shrink * it.base
646 }
647 tentative := it.base
648 if weight > 0 {
649 tentative += free * factor / weight
650 }
651 it.size = math.Max(it.min, math.Min(it.max, safe(tentative)))
652 violation += it.size - tentative
653 }
654 if math.Abs(violation) < 1e-9 {
655 for i := range items {
656 items[i].frozen = true
657 }
658 break
659 }
660 for i := range items {
661 it := &items[i]
662 if it.frozen {
663 continue
664 }
665 factor := it.grow
666 if !grow {
667 factor = it.shrink * it.base
668 }
669 tentative := it.base
670 if weight > 0 {
671 tentative += free * factor / weight
672 }
673 if violation > 0 && it.size > tentative || violation < 0 && it.size < tentative {
674 it.frozen = true
675 }
676 }
677 }
678}
679func (c context) flex(n *Node, r *Result, s css.Style, depth int) error {
680 row := s.FlexDirection != css.KeywordColumn
681 mainSize, crossSize := r.Content.W, r.Content.H
682 gap, crossGap := positive(s.ColumnGap, r.Content.W), positive(s.RowGap, r.Content.W)
683 if !row {
684 mainSize, crossSize = crossSize, mainSize
685 gap, crossGap = crossGap, gap
686 }
687 items := make([]flexItem, 0, len(n.Children))
688 for _, ch := range n.Children {
689 if ch == nil || style(ch).Display == css.KeywordNone {
690 continue
691 }
692 st := style(ch)
693 m := edges(st.Margin, r.Content.W)
694 inW, inH := flexInsets(st, r.Content.W)
695 inMain, inCross := inW, inH
696 if !row {
697 inMain, inCross = inH, inW
698 }
699 it := flexItem{node: ch, grow: safe(st.FlexGrow), shrink: safe(st.FlexShrink)}
700 if row {
701 it.marginMain = m.horizontal()
702 it.marginCross = m.vertical()
703 } else {
704 it.marginMain = m.vertical()
705 it.marginCross = m.horizontal()
706 }
707 dim, crossDim := st.Width, st.Height
708 minCross, maxCross := st.MinHeight, st.MaxHeight
709 if !row {
710 dim, crossDim = st.Height, st.Width
711 minCross, maxCross = st.MinWidth, st.MaxWidth
712 }
713 measureWidth := 0.0
714 if row {
715 measureWidth = mainSize
716 } else {
717 measureWidth = crossSize
718 }
719 measured, _, err := c.intrinsicDepth(ch, measureWidth, 1)
720 if err != nil {
721 return err
722 }
723 intrinsicMain, intrinsicCross := measured.W, measured.H
724 if !row {
725 intrinsicMain, intrinsicCross = measured.H, measured.W
726 }
727 value, ok := length(st.FlexBasis, mainSize)
728 if !ok {
729 value, ok = length(dim, mainSize)
730 }
731 if ok {
732 it.base = safe(value)
733 if st.BoxSizing != css.KeywordBorderBox {
734 it.base += inMain
735 }
736 } else {
737 it.base = intrinsicMain + inMain
738 }
739 value, ok = length(crossDim, crossSize)
740 if ok {
741 it.cross = safe(value)
742 if st.BoxSizing != css.KeywordBorderBox {
743 it.cross += inCross
744 }
745 } else {
746 it.cross = intrinsicCross + inCross
747 }
748 it.min, it.max = flexBounds(st, row, mainSize, inMain)
749 it.cross = boxBound(it.cross, minCross, maxCross, crossSize, inCross, st.BoxSizing == css.KeywordBorderBox)
750 items = append(items, it)
751 }
752 lines := [][]flexItem{}
753 line := []flexItem{}
754 used := 0.0
755 for _, item := range items {
756 need := item.base + item.marginMain
757 if len(line) > 0 {
758 need += gap
759 }
760 if s.FlexWrap == css.KeywordWrap && len(line) > 0 && used+need > mainSize {
761 lines = append(lines, line)
762 line = nil
763 used = 0
764 need = item.base + item.marginMain
765 }
766 line = append(line, item)
767 used += need
768 }
769 if len(line) > 0 {
770 lines = append(lines, line)
771 }
772 crossPos := 0.0
773 for _, line := range lines {
774 lineCross := 0.0
775 for _, it := range line {
776 lineCross = math.Max(lineCross, it.cross+it.marginCross)
777 }
778 if len(lines) == 1 && crossSize > 0 && (row && s.Height.Unit != "auto" && s.Height.Unit != "" || !row && s.Width.Unit != "auto" && s.Width.Unit != "") {
779 lineCross = math.Max(lineCross, crossSize)
780 }
781 resolveFlex(line, mainSize-gap*float64(len(line)-1))
782 occupied := gap * float64(len(line)-1)
783 for _, it := range line {
784 occupied += it.size + it.marginMain
785 }
786 remaining := safe(mainSize - occupied)
787 start, spacing := 0.0, gap
788 switch s.JustifyContent {
789 case css.KeywordFlexEnd:
790 start = remaining
791 case css.KeywordCenter:
792 start = remaining / 2
793 case css.KeywordSpaceBetween:
794 if len(line) > 1 {
795 spacing += remaining / float64(len(line)-1)
796 }
797 case css.KeywordSpaceAround:
798 spacing += remaining / float64(len(line))
799 start = remaining / float64(2*len(line))
800 case css.KeywordSpaceEvenly:
801 spacing += remaining / float64(len(line)+1)
802 start = remaining / float64(len(line)+1)
803 }
804 pos := start
805 for _, it := range line {
806 align := style(it.node).AlignSelf
807 if align == css.KeywordAuto || align == css.KeywordNone {
808 align = s.AlignItems
809 }
810 cross := it.cross
811 if align == css.KeywordStretch {
812 cross = safe(lineCross - it.marginCross)
813 }
814 offset := 0.0
815 switch align {
816 case css.KeywordCenter:
817 offset = safe(lineCross-cross-it.marginCross) / 2
818 case css.KeywordFlexEnd:
819 offset = safe(lineCross - cross - it.marginCross)
820 }
821 main := safe(it.size)
822 cross = safe(cross)
823 var x, y float64
824 var w, h *float64
825 if row {
826 x = r.Content.X + pos
827 y = r.Content.Y + crossPos + offset
828 w = &main
829 h = &cross
830 } else {
831 x = r.Content.X + crossPos + offset
832 y = r.Content.Y + pos
833 w = &cross
834 h = &main
835 }
836 child, err := c.place(it.node, x, y, r.Content.W, r.Content.H, w, h, depth+1)
837 if err != nil {
838 return err
839 }
840 if child != nil {
841 r.Children = append(r.Children, child)
842 cw, ch := envelope(child)
843 r.ContentSize.W = math.Max(r.ContentSize.W, child.Border.X-r.Content.X+cw)
844 r.ContentSize.H = math.Max(r.ContentSize.H, child.Border.Y-r.Content.Y+ch)
845 }
846 pos += main + it.marginMain + spacing
847 }
848 crossPos += lineCross + crossGap
849 }
850 return nil
851}
852
853// slab hands out detached values or sub-slices from one exactly sized block,
854// so a display list costs a few allocations rather than one per shadow, run or
855// text command. Blocks are never resized or reused, so pointers and slices
856// stay valid for the life of the returned Output. Sub-slices have their
857// capacity clipped, so a caller's append cannot overwrite a neighbour. A
858// miscount, or a nil slab, only falls back to individual allocations.
859type slab[T any] struct{ block []T }
860
861func newSlab[T any](n int) *slab[T] {
862 if n <= 0 {
863 return nil
864 }
865 return &slab[T]{block: make([]T, 0, n)}
866}
867
868// one stores v in the slab and returns its address.
869func (a *slab[T]) one(v T) *T {
870 if a == nil || len(a.block) == cap(a.block) {
871 p := new(T) // copy, so v itself never escapes
872 *p = v
873 return p
874 }
875 a.block = append(a.block, v)
876 return &a.block[len(a.block)-1]
877}
878
879// take returns an empty slice with capacity exactly n (nil for n == 0).
880func (a *slab[T]) take(n int) []T {
881 if n <= 0 {
882 return nil
883 }
884 if a == nil || cap(a.block)-len(a.block) < n {
885 return make([]T, 0, n)
886 }
887 i := len(a.block)
888 a.block = a.block[:i+n]
889 return a.block[i : i : i+n]
890}
891
892// radiiOf resolves border-radius percentages against basis.
893func radiiOf(s *css.Style, basis float64) [4]float64 {
894 return [4]float64{positive(s.BorderRadius.TopLeft, basis), positive(s.BorderRadius.TopRight, basis), positive(s.BorderRadius.BottomRight, basis), positive(s.BorderRadius.BottomLeft, basis)}
895}
896
897func hasOutline(s *css.Style, r *Result) bool {
898 return s.OutlineStyle == css.KeywordSolid && positive(s.OutlineWidth, r.Border.W) > 0
899}
900
901// countCommands returns how many commands and shadow copies paint emits for
902// the subtree, so Layout can size the display list once. It mirrors paint's
903// conditions; if it ever drifts, paint still appends correctly and only regrows.
904func countCommands(n *Node, r *Result) (c counts) {
905 if r == nil {
906 return c
907 }
908 s := style(n)
909 total := len(s.BoxShadow)
910 c.shadows = len(s.BoxShadow)
911 if s.BackgroundColor.A > 0 {
912 total++
913 }
914 if r.BorderWidths != (Edges{}) || radiiOf(&s, r.Border.W) != ([4]float64{}) {
915 total++
916 }
917 if hasOutline(&s, r) {
918 total++
919 }
920 if r.Clipped {
921 total += 2
922 }
923 if n.Kind == Image || n.Kind == Text {
924 total++
925 }
926 if n.Kind == Text {
927 c.runs, c.glyphs = textCounts(r)
928 }
929 j := 0
930 for _, child := range n.Children {
931 if child == nil || style(child).Display == css.KeywordNone {
932 continue
933 }
934 if j < len(r.Children) {
935 cc := countCommands(child, r.Children[j])
936 total += cc.commands
937 c.shadows += cc.shadows
938 c.runs += cc.runs
939 c.glyphs += cc.glyphs
940 j++
941 }
942 }
943 c.commands += total
944 return c
945}
946
947type counts struct{ commands, shadows, runs, glyphs int }
948
949func textCounts(r *Result) (runs, glyphs int) {
950 for i := range r.Lines {
951 runs += len(r.Lines[i].Runs)
952 for j := range r.Lines[i].Runs {
953 glyphs += len(r.Lines[i].Runs[j].Glyphs)
954 }
955 }
956 return runs, glyphs
957}
958
959func (c context) paint(n *Node, r *Result, out *[]Command) {
960 if r == nil {
961 return
962 }
963 s := style(n)
964 base := Command{ID: r.ID, Rect: r.Border, Opacity: math.Min(1, safe(s.Opacity))}
965 borderRadii := radiiOf(&s, r.Border.W)
966 for i := range s.BoxShadow {
967 if s.BoxShadow[i].Inset {
968 continue
969 }
970 cmd := base
971 cmd.Op = "shadow"
972 cmd.Shadow = c.shadows.one(s.BoxShadow[i])
973 cmd.Radii = borderRadii
974 *out = append(*out, cmd)
975 }
976 if s.BackgroundColor.A > 0 {
977 cmd := base
978 cmd.Op = "rect"
979 cmd.Color = s.BackgroundColor
980 cmd.Radii = borderRadii
981 *out = append(*out, cmd)
982 }
983 cmd := base
984 cmd.Op = "border"
985 cmd.Widths = r.BorderWidths
986 cmd.Colors = s.BorderColor
987 cmd.Radii = borderRadii
988 if cmd.Widths != (Edges{}) || cmd.Radii != ([4]float64{}) {
989 cmd.Color = s.BorderColor.Top
990 *out = append(*out, cmd)
991 }
992 for i := range s.BoxShadow {
993 if !s.BoxShadow[i].Inset {
994 continue
995 }
996 cmd := base
997 cmd.Op = "shadow"
998 cmd.Shadow = c.shadows.one(s.BoxShadow[i])
999 cmd.Radii = borderRadii
1000 *out = append(*out, cmd)
1001 }
1002 if hasOutline(&s, r) {
1003 cmd = base
1004 cmd.Op = "outline"
1005 cmd.Color = s.OutlineColor
1006 cmd.Radii = borderRadii
1007 width := positive(s.OutlineWidth, r.Border.W)
1008 cmd.Widths = Edges{Top: width, Right: width, Bottom: width, Left: width}
1009 offset := coordValue(s.OutlineOffset, r.Border.W)
1010 cmd.Rect = Rect{X: coord(r.Border.X - offset), Y: coord(r.Border.Y - offset), W: safe(r.Border.W + 2*offset), H: safe(r.Border.H + 2*offset)}
1011 *out = append(*out, cmd)
1012 }
1013 if r.Clipped {
1014 cmd = base
1015 cmd.Op = "clip-push"
1016 cmd.Rect = r.Clip
1017 *out = append(*out, cmd)
1018 }
1019 if n.Kind == Image {
1020 cmd = base
1021 cmd.Op = "image"
1022 cmd.Rect = r.Content
1023 cmd.Image = n.Image
1024 cmd.Radii = radiiOf(&s, r.Content.W)
1025 *out = append(*out, cmd)
1026 }
1027 if n.Kind == Text {
1028 cmd = base
1029 cmd.Op = "text"
1030 cmd.Rect = r.Content
1031 cmd.Text = n.Content
1032 cmd.Color = s.Color
1033 runs, glyphs := textCounts(r)
1034 cmd.Runs = c.runs.take(runs)
1035 backing := c.glyphs.take(glyphs)
1036 for _, line := range r.Lines {
1037 offset := AlignOffset(s.TextAlign, line.Direction, r.Content.W, line.Width)
1038 for _, run := range line.Runs {
1039 rr := Run{Start: run.Start, End: run.End, X: run.X + r.Content.X + offset - r.ScrollX, Y: run.Y + r.Content.Y - r.ScrollY, Advance: run.Advance, Face: run.Face, Size: s.FontSize.Value}
1040 if rr.Size <= 0 {
1041 rr.Size = 16 // matches the intrinsic text.Measure request fallback
1042 }
1043 if run.Face != nil {
1044 rr.FaceID = run.Face.ID
1045 }
1046 if len(run.Glyphs) > 0 { // keep nil for glyph-less runs
1047 first := len(backing)
1048 for _, g := range run.Glyphs {
1049 backing = append(backing, Glyph{uint32(g.ID), g.X + r.Content.X + offset - r.ScrollX, g.Y + r.Content.Y - r.ScrollY, g.Advance, g.Cluster})
1050 }
1051 rr.Glyphs = backing[first:len(backing):len(backing)]
1052 }
1053 cmd.Runs = append(cmd.Runs, rr)
1054 }
1055 }
1056 *out = append(*out, cmd)
1057 }
1058 j := 0
1059 for _, child := range n.Children {
1060 if child == nil || style(child).Display == css.KeywordNone {
1061 continue
1062 }
1063 if j < len(r.Children) {
1064 c.paint(child, r.Children[j], out)
1065 j++
1066 }
1067 }
1068 if r.Clipped {
1069 cmd = base
1070 cmd.Op = "clip-pop"
1071 *out = append(*out, cmd)
1072 }
1073}
1074
1075// AlignOffset is the horizontal offset of a line of width lineW inside a
1076// content box of width contentW under text-align and the line's direction.
1077func AlignOffset(align css.Keyword, direction di.Direction, contentW, lineW float64) float64 {
1078 switch align {
1079 case css.KeywordCenter:
1080 return safe(contentW-lineW) / 2
1081 case css.KeywordRight:
1082 return safe(contentW - lineW)
1083 case css.KeywordStart:
1084 if direction == di.DirectionRTL {
1085 return safe(contentW - lineW)
1086 }
1087 case css.KeywordEnd:
1088 if direction != di.DirectionRTL {
1089 return safe(contentW - lineW)
1090 }
1091 }
1092 return 0
1093}
1094
1095// Hit returns the frontmost node ID whose border box contains the logical point.
1096// Ancestor content clips constrain descendants, but not the ancestor border itself.
1097func Hit(r *Result, x, y float64) string {
1098 if math.IsNaN(x) || math.IsNaN(y) || math.IsInf(x, 0) || math.IsInf(y, 0) {
1099 return ""
1100 }
1101 return hit(r, x, y, Rect{-limit, -limit, 2 * limit, 2 * limit})
1102}
1103func hit(r *Result, x, y float64, clip Rect) string {
1104 if r == nil || !clip.Contains(x, y) {
1105 return ""
1106 }
1107 childClip := clip
1108 if r.Clipped {
1109 childClip = clip.intersect(r.Clip)
1110 }
1111 if childClip.Contains(x, y) {
1112 for i := len(r.Children) - 1; i >= 0; i-- {
1113 if id := hit(r.Children[i], x, y, childClip); id != "" {
1114 return id
1115 }
1116 }
1117 }
1118 if r.Border.Contains(x, y) {
1119 return r.ID
1120 }
1121 return ""
1122}
1123
1124// Physical is the explicit renderer boundary; all layout and hit testing stay logical.
1125// It rounds edges independently to avoid accumulated fractional-scale drift.
1126func Physical(r Rect, scale float64) (x, y, w, h int) {
1127 if math.IsNaN(scale) || math.IsInf(scale, 0) || scale <= 0 {
1128 return
1129 }
1130 scale = math.Min(scale, 1024)
1131 x = int(math.Round(coord(r.X) * scale))
1132 y = int(math.Round(coord(r.Y) * scale))
1133 w = int(math.Round(coord(r.X+safe(r.W))*scale)) - x
1134 h = int(math.Round(coord(r.Y+safe(r.H))*scale)) - y
1135 return
1136}