refactor(ui): make presentation borders canonical
- commit
- 501483979237dd9bae9b864cd94b5dd0b8797433
- author
- brice · · 2026-07-27 15:26 UTC
- committer
- brice · · 2026-07-27 17:06 UTC
- parent
- 1ace180
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +130 -41
M internal/usecase/ui/modal.go +51 -41
| @@ -39,26 +39,14 @@ func (m Modal) Bounds(base domain.Size) domain.Rect { | |||
| 39 | 39 | ||
| 40 | 40 | // Inner returns the modal content rectangle after removing a one-cell border. | |
| 41 | 41 | func (m Modal) Inner(base domain.Size) domain.Rect { | |
| 42 | - | bounds := m.Bounds(base) | |
| 43 | - | return domain.Rect{ | |
| 44 | - | X: bounds.X + 1, | |
| 45 | - | Y: bounds.Y + 1, | |
| 46 | - | Width: max(0, bounds.Width-2), | |
| 47 | - | Height: max(0, bounds.Height-2), | |
| 48 | - | } | |
| 42 | + | return modalInner(m.Bounds(base)) | |
| 49 | 43 | } | |
| 50 | 44 | ||
| 51 | 45 | // Resolve computes the modal's responsive presentation from one preferred | |
| 52 | 46 | // bounds and inner pair. | |
| 53 | 47 | func (m Modal) Resolve(base domain.Size) Presentation { | |
| 54 | 48 | bounds := m.Bounds(base) | |
| 55 | - | inner := domain.Rect{ | |
| 56 | - | X: bounds.X + 1, | |
| 57 | - | Y: bounds.Y + 1, | |
| 58 | - | Width: max(0, bounds.Width-2), | |
| 59 | - | Height: max(0, bounds.Height-2), | |
| 60 | - | } | |
| 61 | - | return ResolvePresentation(base, bounds, inner) | |
| 49 | + | return ResolvePresentation(base, bounds, modalInner(bounds)) | |
| 62 | 50 | } | |
| 63 | 51 | ||
| 64 | 52 | // Composite draws the modal border and title, fills its interior, and returns | |
| @@ -67,12 +55,7 @@ func (m Modal) Resolve(base domain.Size) Presentation { | |||
| 67 | 55 | // Cells outside the modal bounds are not changed. | |
| 68 | 56 | func (m Modal) Composite(f renderer.Frame, border, interior renderer.Style) domain.Rect { | |
| 69 | 57 | bounds := m.Bounds(domain.Size{Cols: f.Width, Rows: f.Height}) | |
| 70 | - | inner := domain.Rect{ | |
| 71 | - | X: bounds.X + 1, | |
| 72 | - | Y: bounds.Y + 1, | |
| 73 | - | Width: max(0, bounds.Width-2), | |
| 74 | - | Height: max(0, bounds.Height-2), | |
| 75 | - | } | |
| 58 | + | inner := modalInner(bounds) | |
| 76 | 59 | DrawBox(f, bounds, border) | |
| 77 | 60 | FillRect(f, inner, renderer.Cell{Rune: ' ', Style: interior}) | |
| 78 | 61 | m.drawTitle(f, bounds, border) | |
| @@ -80,19 +63,13 @@ func (m Modal) Composite(f renderer.Frame, border, interior renderer.Style) doma | |||
| 80 | 63 | } | |
| 81 | 64 | ||
| 82 | 65 | // CompositePresentation draws an already-resolved modal presentation and | |
| 83 | - | // returns its inner content rectangle. Drawers use only top chrome. | |
| 66 | + | // returns its inner content rectangle. | |
| 84 | 67 | func (m Modal) CompositePresentation(f renderer.Frame, p Presentation, border, interior renderer.Style) domain.Rect { | |
| 85 | 68 | FillRect(f, p.Inner, renderer.Cell{Rune: ' ', Style: interior}) | |
| 86 | - | if p.Mode == PresentationDrawer { | |
| 87 | - | if p.Bounds.Width > 0 && p.Bounds.Height > 0 { | |
| 88 | - | for x := p.Bounds.X; x < p.Bounds.X+p.Bounds.Width; x++ { | |
| 89 | - | setCell(f, x, p.Bounds.Y, '─', border) | |
| 90 | - | } | |
| 91 | - | } | |
| 92 | - | } else { | |
| 93 | - | DrawBox(f, p.Bounds, border) | |
| 69 | + | drawBorderEdges(f, p.Bounds, p.Borders, border) | |
| 70 | + | if p.Borders&BorderTop != 0 { | |
| 71 | + | m.drawTitle(f, p.Bounds, border) | |
| 94 | 72 | } | |
| 95 | - | m.drawTitle(f, p.Bounds, border) | |
| 96 | 73 | return p.Inner | |
| 97 | 74 | } | |
| 98 | 75 | ||
| @@ -106,6 +83,15 @@ func (m Modal) drawTitle(f renderer.Frame, bounds domain.Rect, style renderer.St | |||
| 106 | 83 | DrawText(f, start, bounds.Y, right, m.Title, style) | |
| 107 | 84 | } | |
| 108 | 85 | ||
| 86 | + | func modalInner(bounds domain.Rect) domain.Rect { | |
| 87 | + | return domain.Rect{ | |
| 88 | + | X: bounds.X + 1, | |
| 89 | + | Y: bounds.Y + 1, | |
| 90 | + | Width: max(0, bounds.Width-2), | |
| 91 | + | Height: max(0, bounds.Height-2), | |
| 92 | + | } | |
| 93 | + | } | |
| 94 | + | ||
| 109 | 95 | // FillRect fills rect with cell, clipped to the frame bounds. | |
| 110 | 96 | func FillRect(f renderer.Frame, rect domain.Rect, cell renderer.Cell) { | |
| 111 | 97 | left := clamp(rect.X, 0, f.Width) | |
| @@ -121,7 +107,11 @@ func FillRect(f renderer.Frame, rect domain.Rect, cell renderer.Cell) { | |||
| 121 | 107 | ||
| 122 | 108 | // DrawBox draws a single-cell box clipped to the frame bounds. | |
| 123 | 109 | func DrawBox(f renderer.Frame, rect domain.Rect, style renderer.Style) { | |
| 124 | - | if rect.Width <= 0 || rect.Height <= 0 { | |
| 110 | + | drawBorderEdges(f, rect, BorderAll, style) | |
| 111 | + | } | |
| 112 | + | ||
| 113 | + | func drawBorderEdges(f renderer.Frame, rect domain.Rect, edges BorderEdges, style renderer.Style) { | |
| 114 | + | if rect.Width <= 0 || rect.Height <= 0 || edges == 0 { | |
| 125 | 115 | return | |
| 126 | 116 | } | |
| 127 | 117 | left := rect.X | |
| @@ -129,18 +119,38 @@ func DrawBox(f renderer.Frame, rect domain.Rect, style renderer.Style) { | |||
| 129 | 119 | right := rect.X + rect.Width - 1 | |
| 130 | 120 | bottom := rect.Y + rect.Height - 1 | |
| 131 | 121 | ||
| 132 | - | for x := left; x <= right; x++ { | |
| 133 | - | setCell(f, x, top, '─', style) | |
| 134 | - | setCell(f, x, bottom, '─', style) | |
| 122 | + | if edges&BorderTop != 0 { | |
| 123 | + | for x := left; x <= right; x++ { | |
| 124 | + | setCell(f, x, top, '─', style) | |
| 125 | + | } | |
| 126 | + | } | |
| 127 | + | if edges&BorderBottom != 0 { | |
| 128 | + | for x := left; x <= right; x++ { | |
| 129 | + | setCell(f, x, bottom, '─', style) | |
| 130 | + | } | |
| 131 | + | } | |
| 132 | + | if edges&BorderLeft != 0 { | |
| 133 | + | for y := top; y <= bottom; y++ { | |
| 134 | + | setCell(f, left, y, '│', style) | |
| 135 | + | } | |
| 136 | + | } | |
| 137 | + | if edges&BorderRight != 0 { | |
| 138 | + | for y := top; y <= bottom; y++ { | |
| 139 | + | setCell(f, right, y, '│', style) | |
| 140 | + | } | |
| 141 | + | } | |
| 142 | + | if edges&(BorderTop|BorderLeft) == BorderTop|BorderLeft { | |
| 143 | + | setCell(f, left, top, '┌', style) | |
| 144 | + | } | |
| 145 | + | if edges&(BorderTop|BorderRight) == BorderTop|BorderRight { | |
| 146 | + | setCell(f, right, top, '┐', style) | |
| 147 | + | } | |
| 148 | + | if edges&(BorderBottom|BorderLeft) == BorderBottom|BorderLeft { | |
| 149 | + | setCell(f, left, bottom, '└', style) | |
| 135 | 150 | } | |
| 136 | - | for y := top; y <= bottom; y++ { | |
| 137 | - | setCell(f, left, y, '│', style) | |
| 138 | - | setCell(f, right, y, '│', style) | |
| 151 | + | if edges&(BorderBottom|BorderRight) == BorderBottom|BorderRight { | |
| 152 | + | setCell(f, right, bottom, '┘', style) | |
| 139 | 153 | } | |
| 140 | - | setCell(f, left, top, '┌', style) | |
| 141 | - | setCell(f, right, top, '┐', style) | |
| 142 | - | setCell(f, left, bottom, '└', style) | |
| 143 | - | setCell(f, right, bottom, '┘', style) | |
| 144 | 154 | } | |
| 145 | 155 | ||
| 146 | 156 | // DrawText draws text starting at x,y before exclusive clipX and returns the | |
| @@ -39,26 +39,14 @@ func (m Modal) Bounds(base domain.Size) domain.Rect { | |||||
| 39 | 39 | ||||
| 40 | // Inner returns the modal content rectangle after removing a one-cell border. | 40 | // Inner returns the modal content rectangle after removing a one-cell border. | ||
| 41 | func (m Modal) Inner(base domain.Size) domain.Rect { | 41 | func (m Modal) Inner(base domain.Size) domain.Rect { | ||
| 42 | - | bounds := m.Bounds(base) | 42 | + | return modalInner(m.Bounds(base)) |
| 43 | - | return domain.Rect{ | |||
| 44 | - | X: bounds.X + 1, | |||
| 45 | - | Y: bounds.Y + 1, | |||
| 46 | - | Width: max(0, bounds.Width-2), | |||
| 47 | - | Height: max(0, bounds.Height-2), | |||
| 48 | - | } | |||
| 49 | } | 43 | } | ||
| 50 | 44 | ||||
| 51 | // Resolve computes the modal's responsive presentation from one preferred | 45 | // Resolve computes the modal's responsive presentation from one preferred | ||
| 52 | // bounds and inner pair. | 46 | // bounds and inner pair. | ||
| 53 | func (m Modal) Resolve(base domain.Size) Presentation { | 47 | func (m Modal) Resolve(base domain.Size) Presentation { | ||
| 54 | bounds := m.Bounds(base) | 48 | bounds := m.Bounds(base) | ||
| 55 | - | inner := domain.Rect{ | 49 | + | return ResolvePresentation(base, bounds, modalInner(bounds)) |
| 56 | - | X: bounds.X + 1, | |||
| 57 | - | Y: bounds.Y + 1, | |||
| 58 | - | Width: max(0, bounds.Width-2), | |||
| 59 | - | Height: max(0, bounds.Height-2), | |||
| 60 | - | } | |||
| 61 | - | return ResolvePresentation(base, bounds, inner) | |||
| 62 | } | 50 | } | ||
| 63 | 51 | ||||
| 64 | // Composite draws the modal border and title, fills its interior, and returns | 52 | // Composite draws the modal border and title, fills its interior, and returns | ||
| @@ -67,12 +55,7 @@ func (m Modal) Resolve(base domain.Size) Presentation { | |||||
| 67 | // Cells outside the modal bounds are not changed. | 55 | // Cells outside the modal bounds are not changed. | ||
| 68 | func (m Modal) Composite(f renderer.Frame, border, interior renderer.Style) domain.Rect { | 56 | func (m Modal) Composite(f renderer.Frame, border, interior renderer.Style) domain.Rect { | ||
| 69 | bounds := m.Bounds(domain.Size{Cols: f.Width, Rows: f.Height}) | 57 | bounds := m.Bounds(domain.Size{Cols: f.Width, Rows: f.Height}) | ||
| 70 | - | inner := domain.Rect{ | 58 | + | inner := modalInner(bounds) |
| 71 | - | X: bounds.X + 1, | |||
| 72 | - | Y: bounds.Y + 1, | |||
| 73 | - | Width: max(0, bounds.Width-2), | |||
| 74 | - | Height: max(0, bounds.Height-2), | |||
| 75 | - | } | |||
| 76 | DrawBox(f, bounds, border) | 59 | DrawBox(f, bounds, border) | ||
| 77 | FillRect(f, inner, renderer.Cell{Rune: ' ', Style: interior}) | 60 | FillRect(f, inner, renderer.Cell{Rune: ' ', Style: interior}) | ||
| 78 | m.drawTitle(f, bounds, border) | 61 | m.drawTitle(f, bounds, border) | ||
| @@ -80,19 +63,13 @@ func (m Modal) Composite(f renderer.Frame, border, interior renderer.Style) doma | |||||
| 80 | } | 63 | } | ||
| 81 | 64 | ||||
| 82 | // CompositePresentation draws an already-resolved modal presentation and | 65 | // CompositePresentation draws an already-resolved modal presentation and | ||
| 83 | - | // returns its inner content rectangle. Drawers use only top chrome. | 66 | + | // returns its inner content rectangle. |
| 84 | func (m Modal) CompositePresentation(f renderer.Frame, p Presentation, border, interior renderer.Style) domain.Rect { | 67 | func (m Modal) CompositePresentation(f renderer.Frame, p Presentation, border, interior renderer.Style) domain.Rect { | ||
| 85 | FillRect(f, p.Inner, renderer.Cell{Rune: ' ', Style: interior}) | 68 | FillRect(f, p.Inner, renderer.Cell{Rune: ' ', Style: interior}) | ||
| 86 | - | if p.Mode == PresentationDrawer { | 69 | + | drawBorderEdges(f, p.Bounds, p.Borders, border) |
| 87 | - | if p.Bounds.Width > 0 && p.Bounds.Height > 0 { | 70 | + | if p.Borders&BorderTop != 0 { |
| 88 | - | for x := p.Bounds.X; x < p.Bounds.X+p.Bounds.Width; x++ { | 71 | + | m.drawTitle(f, p.Bounds, border) |
| 89 | - | setCell(f, x, p.Bounds.Y, '─', border) | |||
| 90 | - | } | |||
| 91 | - | } | |||
| 92 | - | } else { | |||
| 93 | - | DrawBox(f, p.Bounds, border) | |||
| 94 | } | 72 | } | ||
| 95 | - | m.drawTitle(f, p.Bounds, border) | |||
| 96 | return p.Inner | 73 | return p.Inner | ||
| 97 | } | 74 | } | ||
| 98 | 75 | ||||
| @@ -106,6 +83,15 @@ func (m Modal) drawTitle(f renderer.Frame, bounds domain.Rect, style renderer.St | |||||
| 106 | DrawText(f, start, bounds.Y, right, m.Title, style) | 83 | DrawText(f, start, bounds.Y, right, m.Title, style) | ||
| 107 | } | 84 | } | ||
| 108 | 85 | ||||
| 86 | + | func modalInner(bounds domain.Rect) domain.Rect { | |||
| 87 | + | return domain.Rect{ | |||
| 88 | + | X: bounds.X + 1, | |||
| 89 | + | Y: bounds.Y + 1, | |||
| 90 | + | Width: max(0, bounds.Width-2), | |||
| 91 | + | Height: max(0, bounds.Height-2), | |||
| 92 | + | } | |||
| 93 | + | } | |||
| 94 | + | ||||
| 109 | // FillRect fills rect with cell, clipped to the frame bounds. | 95 | // FillRect fills rect with cell, clipped to the frame bounds. | ||
| 110 | func FillRect(f renderer.Frame, rect domain.Rect, cell renderer.Cell) { | 96 | func FillRect(f renderer.Frame, rect domain.Rect, cell renderer.Cell) { | ||
| 111 | left := clamp(rect.X, 0, f.Width) | 97 | left := clamp(rect.X, 0, f.Width) | ||
| @@ -121,7 +107,11 @@ func FillRect(f renderer.Frame, rect domain.Rect, cell renderer.Cell) { | |||||
| 121 | 107 | ||||
| 122 | // DrawBox draws a single-cell box clipped to the frame bounds. | 108 | // DrawBox draws a single-cell box clipped to the frame bounds. | ||
| 123 | func DrawBox(f renderer.Frame, rect domain.Rect, style renderer.Style) { | 109 | func DrawBox(f renderer.Frame, rect domain.Rect, style renderer.Style) { | ||
| 124 | - | if rect.Width <= 0 || rect.Height <= 0 { | 110 | + | drawBorderEdges(f, rect, BorderAll, style) |
| 111 | + | } | |||
| 112 | + | ||||
| 113 | + | func drawBorderEdges(f renderer.Frame, rect domain.Rect, edges BorderEdges, style renderer.Style) { | |||
| 114 | + | if rect.Width <= 0 || rect.Height <= 0 || edges == 0 { | |||
| 125 | return | 115 | return | ||
| 126 | } | 116 | } | ||
| 127 | left := rect.X | 117 | left := rect.X | ||
| @@ -129,18 +119,38 @@ func DrawBox(f renderer.Frame, rect domain.Rect, style renderer.Style) { | |||||
| 129 | right := rect.X + rect.Width - 1 | 119 | right := rect.X + rect.Width - 1 | ||
| 130 | bottom := rect.Y + rect.Height - 1 | 120 | bottom := rect.Y + rect.Height - 1 | ||
| 131 | 121 | ||||
| 132 | - | for x := left; x <= right; x++ { | 122 | + | if edges&BorderTop != 0 { |
| 133 | - | setCell(f, x, top, '─', style) | 123 | + | for x := left; x <= right; x++ { |
| 134 | - | setCell(f, x, bottom, '─', style) | 124 | + | setCell(f, x, top, '─', style) |
| 125 | + | } | |||
| 126 | + | } | |||
| 127 | + | if edges&BorderBottom != 0 { | |||
| 128 | + | for x := left; x <= right; x++ { | |||
| 129 | + | setCell(f, x, bottom, '─', style) | |||
| 130 | + | } | |||
| 131 | + | } | |||
| 132 | + | if edges&BorderLeft != 0 { | |||
| 133 | + | for y := top; y <= bottom; y++ { | |||
| 134 | + | setCell(f, left, y, '│', style) | |||
| 135 | + | } | |||
| 136 | + | } | |||
| 137 | + | if edges&BorderRight != 0 { | |||
| 138 | + | for y := top; y <= bottom; y++ { | |||
| 139 | + | setCell(f, right, y, '│', style) | |||
| 140 | + | } | |||
| 141 | + | } | |||
| 142 | + | if edges&(BorderTop|BorderLeft) == BorderTop|BorderLeft { | |||
| 143 | + | setCell(f, left, top, '┌', style) | |||
| 144 | + | } | |||
| 145 | + | if edges&(BorderTop|BorderRight) == BorderTop|BorderRight { | |||
| 146 | + | setCell(f, right, top, '┐', style) | |||
| 147 | + | } | |||
| 148 | + | if edges&(BorderBottom|BorderLeft) == BorderBottom|BorderLeft { | |||
| 149 | + | setCell(f, left, bottom, '└', style) | |||
| 135 | } | 150 | } | ||
| 136 | - | for y := top; y <= bottom; y++ { | 151 | + | if edges&(BorderBottom|BorderRight) == BorderBottom|BorderRight { |
| 137 | - | setCell(f, left, y, '│', style) | 152 | + | setCell(f, right, bottom, '┘', style) |
| 138 | - | setCell(f, right, y, '│', style) | |||
| 139 | } | 153 | } | ||
| 140 | - | setCell(f, left, top, '┌', style) | |||
| 141 | - | setCell(f, right, top, '┐', style) | |||
| 142 | - | setCell(f, left, bottom, '└', style) | |||
| 143 | - | setCell(f, right, bottom, '┘', style) | |||
| 144 | } | 154 | } | ||
| 145 | 155 | ||||
| 146 | // DrawText draws text starting at x,y before exclusive clipX and returns the | 156 | // DrawText draws text starting at x,y before exclusive clipX and returns the | ||
M internal/usecase/ui/modal_test.go +79 -0
| @@ -200,6 +200,85 @@ func TestModalResolve(t *testing.T) { | |||
| 200 | 200 | } | |
| 201 | 201 | } | |
| 202 | 202 | ||
| 203 | + | func TestCompositePresentationDrawsFloatingChrome(t *testing.T) { | |
| 204 | + | modal := Modal{WidthPct: 50, FixedHeight: 4, Title: " Rename "} | |
| 205 | + | presentation := modal.Resolve(domain.Size{Cols: 80, Rows: 10}) | |
| 206 | + | frame := renderer.NewFrame(80, 10) | |
| 207 | + | exterior := renderer.Cell{Rune: 'x', Style: renderer.Style{Foreground: 7, Background: 8}} | |
| 208 | + | FillRect(frame, domain.Rect{Width: 80, Height: 10}, exterior) | |
| 209 | + | border := renderer.Style{Bold: true, Foreground: 2, Background: -1} | |
| 210 | + | interior := renderer.Style{Foreground: 3, Background: 4} | |
| 211 | + | ||
| 212 | + | inner := modal.CompositePresentation(frame, presentation, border, interior) | |
| 213 | + | require.Equal(t, presentation.Inner, inner) | |
| 214 | + | require.Equal(t, '┌', frame.At(presentation.Bounds.X, presentation.Bounds.Y).Rune) | |
| 215 | + | require.Equal(t, '┐', frame.At(presentation.Bounds.X+presentation.Bounds.Width-1, presentation.Bounds.Y).Rune) | |
| 216 | + | require.Equal(t, '└', frame.At(presentation.Bounds.X, presentation.Bounds.Y+presentation.Bounds.Height-1).Rune) | |
| 217 | + | require.Equal(t, '┘', frame.At(presentation.Bounds.X+presentation.Bounds.Width-1, presentation.Bounds.Y+presentation.Bounds.Height-1).Rune) | |
| 218 | + | require.Equal(t, 'R', frame.At(37, presentation.Bounds.Y).Rune) | |
| 219 | + | require.True(t, frame.At(inner.X, inner.Y).Style.Equal(interior)) | |
| 220 | + | assertCell(t, frame, presentation.Bounds.X-1, presentation.Bounds.Y, exterior) | |
| 221 | + | } | |
| 222 | + | ||
| 223 | + | func TestCompositePresentationUsesBordersRatherThanMode(t *testing.T) { | |
| 224 | + | border := renderer.Style{Bold: true, Foreground: 2, Background: -1} | |
| 225 | + | exterior := renderer.Cell{Rune: 'x', Style: renderer.Style{Foreground: 7, Background: 8}} | |
| 226 | + | bounds := domain.Rect{X: 1, Y: 1, Width: 8, Height: 4} | |
| 227 | + | inner := domain.Rect{X: 2, Y: 2, Width: 6, Height: 2} | |
| 228 | + | ||
| 229 | + | tests := []struct { | |
| 230 | + | name string | |
| 231 | + | presentation Presentation | |
| 232 | + | assert func(*testing.T, renderer.Frame) | |
| 233 | + | }{ | |
| 234 | + | { | |
| 235 | + | name: "all edges render for drawer mode", | |
| 236 | + | presentation: Presentation{ | |
| 237 | + | Mode: PresentationDrawer, Bounds: bounds, Inner: inner, Borders: BorderAll, | |
| 238 | + | }, | |
| 239 | + | assert: func(t *testing.T, frame renderer.Frame) { | |
| 240 | + | require.Equal(t, '┌', frame.At(1, 1).Rune) | |
| 241 | + | require.Equal(t, '│', frame.At(1, 2).Rune) | |
| 242 | + | require.Equal(t, '┘', frame.At(8, 4).Rune) | |
| 243 | + | require.Equal(t, 'T', frame.At(2, 1).Rune) | |
| 244 | + | }, | |
| 245 | + | }, | |
| 246 | + | { | |
| 247 | + | name: "top edge alone renders for floating mode", | |
| 248 | + | presentation: Presentation{ | |
| 249 | + | Mode: PresentationFloating, Bounds: bounds, Inner: inner, Borders: BorderTop, | |
| 250 | + | }, | |
| 251 | + | assert: func(t *testing.T, frame renderer.Frame) { | |
| 252 | + | require.Equal(t, '─', frame.At(1, 1).Rune) | |
| 253 | + | require.Equal(t, 'T', frame.At(2, 1).Rune) | |
| 254 | + | assertCell(t, frame, 1, 2, exterior) | |
| 255 | + | assertCell(t, frame, 8, 4, exterior) | |
| 256 | + | }, | |
| 257 | + | }, | |
| 258 | + | { | |
| 259 | + | name: "title is hidden without top edge", | |
| 260 | + | presentation: Presentation{ | |
| 261 | + | Mode: PresentationFloating, Bounds: bounds, Inner: inner, Borders: BorderLeft, | |
| 262 | + | }, | |
| 263 | + | assert: func(t *testing.T, frame renderer.Frame) { | |
| 264 | + | require.Equal(t, '│', frame.At(1, 1).Rune) | |
| 265 | + | require.Equal(t, '│', frame.At(1, 4).Rune) | |
| 266 | + | assertCell(t, frame, 4, 1, exterior) | |
| 267 | + | assertCell(t, frame, 8, 4, exterior) | |
| 268 | + | }, | |
| 269 | + | }, | |
| 270 | + | } | |
| 271 | + | ||
| 272 | + | for _, tt := range tests { | |
| 273 | + | t.Run(tt.name, func(t *testing.T) { | |
| 274 | + | frame := renderer.NewFrame(10, 6) | |
| 275 | + | FillRect(frame, domain.Rect{Width: 10, Height: 6}, exterior) | |
| 276 | + | (Modal{Title: "Title"}).CompositePresentation(frame, tt.presentation, border, renderer.DefaultStyle()) | |
| 277 | + | tt.assert(t, frame) | |
| 278 | + | }) | |
| 279 | + | } | |
| 280 | + | } | |
| 281 | + | ||
| 203 | 282 | func TestCompositeDrawerChromeAndPreservesExterior(t *testing.T) { | |
| 204 | 283 | modal := Modal{FixedHeight: 4, Title: " Rename "} | |
| 205 | 284 | presentation := modal.Resolve(domain.Size{Cols: 20, Rows: 10}) | |
| @@ -200,6 +200,85 @@ func TestModalResolve(t *testing.T) { | |||||
| 200 | } | 200 | } | ||
| 201 | } | 201 | } | ||
| 202 | 202 | ||||
| 203 | + | func TestCompositePresentationDrawsFloatingChrome(t *testing.T) { | |||
| 204 | + | modal := Modal{WidthPct: 50, FixedHeight: 4, Title: " Rename "} | |||
| 205 | + | presentation := modal.Resolve(domain.Size{Cols: 80, Rows: 10}) | |||
| 206 | + | frame := renderer.NewFrame(80, 10) | |||
| 207 | + | exterior := renderer.Cell{Rune: 'x', Style: renderer.Style{Foreground: 7, Background: 8}} | |||
| 208 | + | FillRect(frame, domain.Rect{Width: 80, Height: 10}, exterior) | |||
| 209 | + | border := renderer.Style{Bold: true, Foreground: 2, Background: -1} | |||
| 210 | + | interior := renderer.Style{Foreground: 3, Background: 4} | |||
| 211 | + | ||||
| 212 | + | inner := modal.CompositePresentation(frame, presentation, border, interior) | |||
| 213 | + | require.Equal(t, presentation.Inner, inner) | |||
| 214 | + | require.Equal(t, '┌', frame.At(presentation.Bounds.X, presentation.Bounds.Y).Rune) | |||
| 215 | + | require.Equal(t, '┐', frame.At(presentation.Bounds.X+presentation.Bounds.Width-1, presentation.Bounds.Y).Rune) | |||
| 216 | + | require.Equal(t, '└', frame.At(presentation.Bounds.X, presentation.Bounds.Y+presentation.Bounds.Height-1).Rune) | |||
| 217 | + | require.Equal(t, '┘', frame.At(presentation.Bounds.X+presentation.Bounds.Width-1, presentation.Bounds.Y+presentation.Bounds.Height-1).Rune) | |||
| 218 | + | require.Equal(t, 'R', frame.At(37, presentation.Bounds.Y).Rune) | |||
| 219 | + | require.True(t, frame.At(inner.X, inner.Y).Style.Equal(interior)) | |||
| 220 | + | assertCell(t, frame, presentation.Bounds.X-1, presentation.Bounds.Y, exterior) | |||
| 221 | + | } | |||
| 222 | + | ||||
| 223 | + | func TestCompositePresentationUsesBordersRatherThanMode(t *testing.T) { | |||
| 224 | + | border := renderer.Style{Bold: true, Foreground: 2, Background: -1} | |||
| 225 | + | exterior := renderer.Cell{Rune: 'x', Style: renderer.Style{Foreground: 7, Background: 8}} | |||
| 226 | + | bounds := domain.Rect{X: 1, Y: 1, Width: 8, Height: 4} | |||
| 227 | + | inner := domain.Rect{X: 2, Y: 2, Width: 6, Height: 2} | |||
| 228 | + | ||||
| 229 | + | tests := []struct { | |||
| 230 | + | name string | |||
| 231 | + | presentation Presentation | |||
| 232 | + | assert func(*testing.T, renderer.Frame) | |||
| 233 | + | }{ | |||
| 234 | + | { | |||
| 235 | + | name: "all edges render for drawer mode", | |||
| 236 | + | presentation: Presentation{ | |||
| 237 | + | Mode: PresentationDrawer, Bounds: bounds, Inner: inner, Borders: BorderAll, | |||
| 238 | + | }, | |||
| 239 | + | assert: func(t *testing.T, frame renderer.Frame) { | |||
| 240 | + | require.Equal(t, '┌', frame.At(1, 1).Rune) | |||
| 241 | + | require.Equal(t, '│', frame.At(1, 2).Rune) | |||
| 242 | + | require.Equal(t, '┘', frame.At(8, 4).Rune) | |||
| 243 | + | require.Equal(t, 'T', frame.At(2, 1).Rune) | |||
| 244 | + | }, | |||
| 245 | + | }, | |||
| 246 | + | { | |||
| 247 | + | name: "top edge alone renders for floating mode", | |||
| 248 | + | presentation: Presentation{ | |||
| 249 | + | Mode: PresentationFloating, Bounds: bounds, Inner: inner, Borders: BorderTop, | |||
| 250 | + | }, | |||
| 251 | + | assert: func(t *testing.T, frame renderer.Frame) { | |||
| 252 | + | require.Equal(t, '─', frame.At(1, 1).Rune) | |||
| 253 | + | require.Equal(t, 'T', frame.At(2, 1).Rune) | |||
| 254 | + | assertCell(t, frame, 1, 2, exterior) | |||
| 255 | + | assertCell(t, frame, 8, 4, exterior) | |||
| 256 | + | }, | |||
| 257 | + | }, | |||
| 258 | + | { | |||
| 259 | + | name: "title is hidden without top edge", | |||
| 260 | + | presentation: Presentation{ | |||
| 261 | + | Mode: PresentationFloating, Bounds: bounds, Inner: inner, Borders: BorderLeft, | |||
| 262 | + | }, | |||
| 263 | + | assert: func(t *testing.T, frame renderer.Frame) { | |||
| 264 | + | require.Equal(t, '│', frame.At(1, 1).Rune) | |||
| 265 | + | require.Equal(t, '│', frame.At(1, 4).Rune) | |||
| 266 | + | assertCell(t, frame, 4, 1, exterior) | |||
| 267 | + | assertCell(t, frame, 8, 4, exterior) | |||
| 268 | + | }, | |||
| 269 | + | }, | |||
| 270 | + | } | |||
| 271 | + | ||||
| 272 | + | for _, tt := range tests { | |||
| 273 | + | t.Run(tt.name, func(t *testing.T) { | |||
| 274 | + | frame := renderer.NewFrame(10, 6) | |||
| 275 | + | FillRect(frame, domain.Rect{Width: 10, Height: 6}, exterior) | |||
| 276 | + | (Modal{Title: "Title"}).CompositePresentation(frame, tt.presentation, border, renderer.DefaultStyle()) | |||
| 277 | + | tt.assert(t, frame) | |||
| 278 | + | }) | |||
| 279 | + | } | |||
| 280 | + | } | |||
| 281 | + | ||||
| 203 | func TestCompositeDrawerChromeAndPreservesExterior(t *testing.T) { | 282 | func TestCompositeDrawerChromeAndPreservesExterior(t *testing.T) { | ||
| 204 | modal := Modal{FixedHeight: 4, Title: " Rename "} | 283 | modal := Modal{FixedHeight: 4, Title: " Rename "} | ||
| 205 | presentation := modal.Resolve(domain.Size{Cols: 20, Rows: 10}) | 284 | presentation := modal.Resolve(domain.Size{Cols: 20, Rows: 10}) | ||