| 1 | package layout |
| 2 | |
| 3 | // regions.go is where each flow template puts its regions, at 1920 by |
| 4 | // 1080, and it is the ONE place that is written down (the slice 3 plan |
| 5 | // says so). Everything else in this package asks for a region by name. |
| 6 | // |
| 7 | // It is a reading of internal/slides/static/slides.css's |
| 8 | // `grid-template-areas`: the stylesheet is the truth, this is that |
| 9 | // truth evaluated at one size. regions_test.go parses the stylesheet |
| 10 | // and holds the two to the same set of region names per template — a |
| 11 | // structural guard, which is the honest one to make there — and |
| 12 | // internal/slidestest's browser drive checks the boxes themselves |
| 13 | // against a real Chromium at 1920. |
| 14 | // |
| 15 | // A table cannot express a grid's `auto` row, which is as tall as |
| 16 | // whatever ends up in it, so the numbers here are the one-line case |
| 17 | // and engine.place adjusts two of them once the content has been |
| 18 | // measured: `title` centres its two rows as a pair, and a heading that |
| 19 | // runs to two lines pushes `title-body`'s and `two-column`'s lower row |
| 20 | // down instead of overlapping it. This table is where they start. |
| 21 | |
| 22 | // Align is where a region's content sits inside the region's box: the |
| 23 | // stack of blocks in the block direction, and each line of text in the |
| 24 | // inline direction. |
| 25 | type Align uint8 |
| 26 | |
| 27 | const ( |
| 28 | // Start is the top, or the reading-order start of a line. |
| 29 | Start Align = iota |
| 30 | // Center is `align-content: center` / `text-align: center`. |
| 31 | Center |
| 32 | // End is the bottom, or the end of a line. |
| 33 | End |
| 34 | ) |
| 35 | |
| 36 | // Box is a rectangle in frame coordinates. Not the Rect op — that one |
| 37 | // is something to draw; this one is somewhere to draw in. |
| 38 | type Box struct{ X, Y, W, H float64 } |
| 39 | |
| 40 | // Bottom and Right are the two edges the flow code keeps asking for. |
| 41 | func (b Box) Bottom() float64 { return b.Y + b.H } |
| 42 | func (b Box) Right() float64 { return b.X + b.W } |
| 43 | |
| 44 | // Region is one grid area: where it is, and how what goes in it sits. |
| 45 | type Region struct { |
| 46 | Box |
| 47 | // AlignY is how the stack of blocks sits in the box, and AlignX is |
| 48 | // how each line sits in its own width. A block with its own |
| 49 | // `align` prop overrides AlignX for that block alone (§2.4). |
| 50 | AlignY, AlignX Align |
| 51 | } |
| 52 | |
| 53 | // The content box: the frame less the section's own padding. |
| 54 | const ( |
| 55 | contentX = pad |
| 56 | contentY = pad |
| 57 | contentW = FrameW - 2*pad |
| 58 | contentH = FrameH - 2*pad |
| 59 | ) |
| 60 | |
| 61 | // The derived measurements each template's grid needs. The type sizes |
| 62 | // are the padded scale, because every template that has a heading row |
| 63 | // keeps the section's padding; image-full's are the full one, which is |
| 64 | // what `padding: 0` gives it (sizes.go says why there are two). |
| 65 | const ( |
| 66 | // headingRow is what a template's `grid-template-rows: auto 1fr` |
| 67 | // gives its heading: one line of the biggest type. A heading that |
| 68 | // runs to two lines overflows into the body region and says so — |
| 69 | // which is what the browser does not do (its `auto` row grows), |
| 70 | // and is the largest single divergence in this table. |
| 71 | headingRow = 6.6 * cqwPadded * leadHeading |
| 72 | |
| 73 | // titleRow is half the content box, for `title`'s two auto rows. |
| 74 | titleRow = (contentH - sectionGap) / 2 |
| 75 | // titleW is the `max-inline-size: 80%` those rows are held to. |
| 76 | // The browser shrink-wraps the region to its own content and |
| 77 | // centres that; centring the same lines inside the full 80% puts |
| 78 | // them in the same place, and wraps at the same width. |
| 79 | titleW = 0.80 * contentW |
| 80 | titleX = contentX + (contentW-titleW)/2 |
| 81 | |
| 82 | // two-column's `column-gap`, which is written on the container — |
| 83 | // see sizes.go's sectionColGap. |
| 84 | colW = (contentW - sectionColGap) / 2 |
| 85 | |
| 86 | // quote's `max-inline-size: 84%`. |
| 87 | quoteW = 0.84 * contentW |
| 88 | quoteX = contentX + (contentW-quoteW)/2 |
| 89 | |
| 90 | // image-full's heading sits over the picture's bottom-left corner: |
| 91 | // `margin: var(--slides-pad)`, `padding: calc(gap/2) gap`, |
| 92 | // `max-inline-size: 80%`. scrimW is the outer cap; the box below |
| 93 | // is the text inside the padding. |
| 94 | scrimW = 0.80 * FrameW |
| 95 | scrimPadX = 2.6 * cqwFull |
| 96 | scrimPadY = scrimPadX / 2 |
| 97 | scrimRow = 5.0 * cqwFull * leadHeading |
| 98 | ) |
| 99 | |
| 100 | // regions is the table. A template is a set of named areas; a region |
| 101 | // name a template has no area for is not in the map, and regionFor |
| 102 | // falls back the way the stylesheet does (auto placement into the one |
| 103 | // column every template has). |
| 104 | var regions = map[string]map[string]Region{ |
| 105 | // title — everything centred, in the middle. Two auto rows under |
| 106 | // `align-content: center`, so the heading hangs from the bottom of |
| 107 | // the top half and the body sits at the top of the lower one. |
| 108 | "title": { |
| 109 | "heading": {Box{titleX, contentY, titleW, titleRow}, End, Center}, |
| 110 | "body": {Box{titleX, contentY + titleRow + sectionGap, titleW, titleRow}, Start, Center}, |
| 111 | }, |
| 112 | // title-body — the workhorse: `grid-template-rows: auto 1fr`. |
| 113 | "title-body": { |
| 114 | "heading": {Box{contentX, contentY, contentW, headingRow}, Start, Start}, |
| 115 | "body": {Box{contentX, contentY + headingRow + sectionGap, contentW, contentH - headingRow - sectionGap}, Start, Start}, |
| 116 | }, |
| 117 | // two-column — the heading spans, body and aside sit side by side. |
| 118 | "two-column": { |
| 119 | "heading": {Box{contentX, contentY, contentW, headingRow}, Start, Start}, |
| 120 | "body": {Box{contentX, contentY + headingRow + sectionGap, colW, contentH - headingRow - sectionGap}, Start, Start}, |
| 121 | "aside": {Box{contentX + colW + sectionColGap, contentY + headingRow + sectionGap, colW, contentH - headingRow - sectionGap}, Start, Start}, |
| 122 | }, |
| 123 | // section — one heading, centred, over the accent bar drawn by the |
| 124 | // region's ::after (the only decoration in the whole stylesheet, |
| 125 | // and blocks.go draws it). |
| 126 | "section": { |
| 127 | "heading": {Box{contentX, contentY, contentW, contentH}, Center, Center}, |
| 128 | }, |
| 129 | // image-full — the media fills the frame (`padding: 0`), and the |
| 130 | // heading is placed into the same area over it. |
| 131 | "image-full": { |
| 132 | "media": {Box{0, 0, FrameW, FrameH}, Start, Start}, |
| 133 | "heading": {Box{contentX + scrimPadX, FrameH - pad - scrimPadY - scrimRow, scrimW - 2*scrimPadX, scrimRow}, End, Start}, |
| 134 | }, |
| 135 | // quote — one pull quote, centred, nothing competing with it. |
| 136 | "quote": { |
| 137 | "body": {Box{quoteX, contentY, quoteW, contentH}, Center, Center}, |
| 138 | }, |
| 139 | // blank — a single body region, stacked from the top. It is also |
| 140 | // what a canvas slide reflowed onto a phone becomes, though print |
| 141 | // never sees that (§2.5). |
| 142 | "blank": { |
| 143 | "body": {Box{contentX, contentY, contentW, contentH}, Start, Start}, |
| 144 | }, |
| 145 | } |
| 146 | |
| 147 | // regionOrder is the order regions are drawn in, matching |
| 148 | // render.regionOrder — the reading order a screen reader hears, and |
| 149 | // therefore the order a paint list stacks in. |
| 150 | var regionOrder = []string{"heading", "body", "aside", "media"} |
| 151 | |
| 152 | // regionFor is where a template puts a named region. |
| 153 | // |
| 154 | // A name the template has no area for falls back to its body, or to |
| 155 | // the whole content box: the stylesheet auto-places such a region into |
| 156 | // the single column every template's grid has, so it lands where the |
| 157 | // body would. An aside on a title slide is unusual, not an error |
| 158 | // (render's own comment: it is what keeps it readable). |
| 159 | func regionFor(template, name string) Region { |
| 160 | byName, ok := regions[template] |
| 161 | if !ok { |
| 162 | byName = regions["blank"] |
| 163 | } |
| 164 | if r, ok := byName[name]; ok { |
| 165 | return r |
| 166 | } |
| 167 | if r, ok := byName["body"]; ok { |
| 168 | return r |
| 169 | } |
| 170 | return Region{Box{contentX, contentY, contentW, contentH}, Start, Start} |
| 171 | } |
| 172 | |
| 173 | // Templates is the flow templates this table covers, in the order |
| 174 | // slides.css declares them. It matches render's own vocabulary; a |
| 175 | // template outside it renders as blank there and lays out as blank |
| 176 | // here. |
| 177 | func Templates() []string { |
| 178 | return []string{"title", "title-body", "two-column", "section", "image-full", "quote", "blank"} |
| 179 | } |
| 180 | |