# Glass ## Laying out composite controls Glass has no layout pass. A control that arranges children (`FileDialog`, `MessageBox`, `PopupMenu`) does two things itself, from the same carving tools, so the two can't drift apart: 1. **Measure.** Work out how big each part wants to be, and the smallest the whole can be. 2. **Arrange.** Carve the space it has into rects, and assign each child's `Bounds`. Don't do either with hand arithmetic: `n * (width + spacing)`, `2 * spacing`, `3 * spacing` for "the gaps I counted", `indent = icon + spacing`. A count like that goes stale when a part is added, and nothing fails. Let a tool count. ### The two tools | | `RectAggregator` (`RectAggregator.cs`) | `rect.NewRow` / `rect.NewCol` (`RectExtensions.cs`) | |---|---|---| | Use it to | **measure**: it grows as items are added | **arrange**: it shrinks as parts are carved off | | `margin` | goes *between* items, and the first is flush | goes *after* the carved piece, before what's left | | Result | read `.Rect.Size`, or `.Width` and `.Height` | each call returns the piece, and the local keeps the rest | - A `RectAggregator` takes its default gap as a `Vector`, per axis. Use `new Vector(spacing, 0)` for things side by side and `new Vector(0, spacing)` for a stack. A call's own `margin:` overrides it, and `margin: 0` butts two rows. - `NewCols(width, count)` adds a run of equal columns, with the gaps between them counted. - The extensions take `ref this Rect`, so call them on a **local variable**, never on a property. Each call consumes what it carves. What's left after the last call is the part that takes the rest, such as a list or a text box, so assign it directly. - Pass `Shortfall.UseWhatIsLeft` when the control can be dragged below its minimum, or a too-small rect throws. - For equal parts, `rect.NewCols(count, margin)`, `NewRows` and `NewGrid` return a `RectDivider` you index. - `rect.Erode(new Rect.BorderSize(spacing))` is the padding around the edge. ### Measure, then arrange Measure into fields, then arrange from them. A part that takes the rest of its row has no minimum width, but the gap before it still counts, so add it as a zero-width column. ```csharp // Measure: the smallest the control can be. var toolbar = new RectAggregator(new Vector(spacing, 0)); toolbar.NewCols(buttonWidth, buttonCount); toolbar.NewCol(0); // the path box, which takes the rest, and the gap before it var stack = new RectAggregator(new Vector(0, spacing)); stack.NewRow(toolbarHeight); stack.NewRow(listHeight, margin: 0); // the list butts against its header stack.NewRow(buttonRowHeight); Rect content = Rect.FromPositionAndSize( Vector.Zero, new Vector(Scalar.Max(toolbar.Rect.Width, listWidth), stack.Rect.Height)); MinContentSize = content.Expand(new Rect.BorderSize(spacing)).Size; // the edge padding, as Erode takes it off below // Arrange: the same parts, carved out of the space there is. Rect remainder = Rect.FromPositionAndSize(Vector.Zero, contentSize).Erode(new Rect.BorderSize(spacing)); Rect toolbarRow = remainder.NewRow(toolbarHeight, VerticalJustification.Top, spacing, Shortfall.UseWhatIsLeft); Rect buttonRow = remainder.NewRow(buttonRowHeight, VerticalJustification.Bottom, spacing, Shortfall.UseWhatIsLeft); BackButton.Bounds = toolbarRow.NewCol(buttonWidth, HorizontalJustification.Left, spacing, Shortfall.UseWhatIsLeft); PathBox.Bounds = toolbarRow; // what's left of the row EntryList.Bounds = remainder; // what's left of everything ``` Keep the order of parts the same in both halves. Measure and arrange then agree, and a layout test that pins one catches a drift in the other. `MessageBox.Layout` is the smallest worked example, `PopupMenu.Layout` is next, and `FileDialog.Measure`, `MinimumContentSize` and `ArrangeParts` are the full one. ### Sizes, materials and coordinates - A part's size comes from its material: `material.PreferredSize(Constraints.Unbounded)`. `BoundsSizeFor(viewportSize)` adds a material's chrome around a content size, and `ViewportSizeFor(boundsSize)` takes it away again. - A line of text is `TextMaterialStyle.SingleLineHeight`, which includes the style's margins and scale. Don't add `LineHeight` and margins yourself. - A child's `Bounds` is in its parent's `CanvasRect`, which is inside the parent's material border. Carve from `CanvasRect`, or from `ContentSize` in a `Window`, and never from the parent's own `Bounds`. - A `Window` takes its minimum in `MinContentSize`, and raises `OnContentResized` whenever the content area changes. Measure when a material, a text or a part's visibility changes, and arrange on `OnContentResized`. - Centring has no helper. `VerticalJustification` is `Top` or `Bottom` only, so `(outer - inner) / 2` on one axis is the one hand-written offset that's expected. - Layout only assigns `Bounds`. It never creates or removes a control. ### Testing a layout Pin the minimum size and each part's `Bounds` with numbers worked out by hand. Use test materials whose `PreferredSize` is fixed, such as `ButtonOf(46, 16)` in `FileDialogTests`, so a failure says which part moved. ## Thorough tests A few Glass tests carry `[Thorough]`, and the runner skips them unless given `--thorough`. A normal build runs the suite without it, and says nothing about the tests it left out. The thorough ones are the extra `OnLongLines_*` sweeps in `Typesetting/TextViewportTests.cs`. Each lays a long line out in stretches and compares the answers with laying out the whole line. A mutation pass and a coverage run showed that five of the sixteen `OnLongLines_*` tests catch every mutant any of them catch and cover every line and branch they cover. The other eleven add nothing those five don't, but they cost about 0.4s of the suite's ~9s. The same holds for sweeps in `Typesetting/VisualLineIndexTests.cs`. Nine in `NoteEditTests` (the `…_AnywhereInAText` and `…_InAPartlyCountedText` sweeps, except the line feed, carriage return and CR LF ones that insert anywhere) and fifteen in `ChunkedLinesTests` (the `MatchAFullLayout_*` widths and the `Inserting/Replacing/Deleting/Editing…MatchesAFullLayout` edits) are thorough. The tests left running catch every edit-path mutant they catch and cover the same lines. In `Editing/EditBufferTests.cs`, 25 of the `WindowCharsTests` sweeps are thorough: the `…MatchesWholeLines` ones for text shapes and scripts. The five kept are `RunsOfSpaces_`, `CombiningMarks_`, `Cyrillic_`, `HardBreaksOfEveryKind_` and `InsertingAndReplacingInMarksAndPairs_`. They catch every window-cut and resume mutant any of the 30 caught. In `WidthChunkedLinesTests`, seven sweeps are thorough: the `InsertingAChar_`, `InsertingAWord_`, `DeletingSomeChars_` and `ReplacingAChar_` edit sweeps, and the `SpacedTexts_MatchAFullLayout`, `ShortLines_` and `OddTexts_…WithTrailingSpacesWrapping` text sweeps. The rest catch every chunk-width, kerning and hang mutant they did. In `TextViewportTests.ScrollToCaretTests`, 14 more are thorough: eight of the nine `WideLines_…` sweeps (`_Third` stays, for the empty-row break stub), three of the four `InABoxOfOneLine_…` ones (`_Centred` stays), and `SidewaysOnAWideLineUnderComposition_InsertingInTheMiddle_` and `_WhenTheLineEndsInSpaces_` and `SidewaysOnAShortLineUnderComposition_`. The sideways scroll, alignment and segmented-window mutants they caught are still caught. In `SplicedRowsTests`, six are thorough: four of the `RowOfTests` per-replacement sweeps (the char and wrapped-lines ones stay) and two `TotalRowsTests` chunked-line tests, `OnAChunkedLine_WhenTheRowsAfterAreShifted_` and `OnAChunkedLine_AmongShortOnes_`. `OnAChunkedLineOfProse_…` stays, as the only one left that splices at a chunk edge. In `Editing/SplicedTextTests.cs`, `AgreesWithAStringSourceTests.OnTheLineCountAndNumbersInATextOfManyLines` is thorough. `OnTheLineQueriesAtEveryIndex` runs over the paged source and insertions up to 100 chars. Longer ones and the string source are covered by `OnTheLineQueriesInLongLines` and the line-count tests. Every `SplicedText.cs` mutant caught before is still caught. Two random-text tests are gone, `LineBreakIteratorTests.ByRulesAloneTests.AgreesWithThePairTable_OnRandomText` and `LineLayoutTests.Unconstrained_AgreesWithTheGeneralPath_OnRandomText`. Every `LineBreakPairTable` and `LineLayout` mutant they caught is caught by a named test, and the one line only they covered, the LB25 `PR OP IS NU` path, now has `AffixBeforeABracketAndASeparatedNumber_StaysWithThem` and `…ASeparatorWithoutANumber_CanBreak`. **Run them with `--thorough` when you change the stretch or row-walk code**, which is `TextViewport.cs`, `VisualLineIndex.cs` and `SplicedRows.cs` under `Typesetting/`. Run the built exe by absolute path, from the repo root: ``` \Junk\Glass.UnitTests\release\Glass.UnitTests.exe --thorough ``` `python -u Scripts/coverage.py` passes `--thorough` itself, so coverage counts them. Other runner options: - `--skip=` leaves out tests whose full name matches. The name is the namespace, the nested fixtures, then the method, all joined by dots, as in `Glass.UnitTests.Typesetting.TextViewportTests.LineEdgeTests.OnLongLines_X`. Give it twice to skip either match. `coverage.py --test-arguments=--skip=` measures coverage with tests left out. - `--times` prints every fixture's and test's time, slowest first. ## Marking a test `[Thorough]` Mark one only after showing that the tests left running catch what it does. Make one-line edits to the code it covers, in a scratch workspace, and see that another test fails for each. Then run `coverage.py` with and without the test and diff the covered lines. Don't mark a test just because it is slow. A thorough test that nothing else backs up is a gap that stays hidden until someone remembers to pass the flag.