refactor(core): compute focus separators in core and send them in the scene
- commit
- de25548355269e17436d40308cda3aa4a8cc92ab
- author
- brice · · 2026-09-26 15:09 UTC
- parent
- 0e47d2a
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
9 files · +260 -150
M internal/adapters/drm/overlay.go +2 -2
| @@ -91,8 +91,8 @@ func overlayCandidate(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceCo | |||
| 91 | 91 | return ports.SceneWindow{}, ports.SurfaceContent{}, "layer_above" | |
| 92 | 92 | } | |
| 93 | 93 | } | |
| 94 | - | if !pick.Fullscreen && pick.Inset|pick.Neighbors != 0 && s.Border.Width > 0 { | |
| 95 | - | // The overlay shows the buffer alone; its border would go with it. | |
| 94 | + | if !pick.Fullscreen && len(s.Separators) > 0 { | |
| 95 | + | // The overlay shows the buffer alone; the lines would go with it. | |
| 96 | 96 | return ports.SceneWindow{}, ports.SurfaceContent{}, "border" | |
| 97 | 97 | } | |
| 98 | 98 | c := surfaces[pick.ID] | |
| @@ -91,8 +91,8 @@ func overlayCandidate(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceCo | |||||
| 91 | return ports.SceneWindow{}, ports.SurfaceContent{}, "layer_above" | 91 | return ports.SceneWindow{}, ports.SurfaceContent{}, "layer_above" | ||
| 92 | } | 92 | } | ||
| 93 | } | 93 | } | ||
| 94 | - | if !pick.Fullscreen && pick.Inset|pick.Neighbors != 0 && s.Border.Width > 0 { | 94 | + | if !pick.Fullscreen && len(s.Separators) > 0 { |
| 95 | - | // The overlay shows the buffer alone; its border would go with it. | 95 | + | // The overlay shows the buffer alone; the lines would go with it. |
| 96 | return ports.SceneWindow{}, ports.SurfaceContent{}, "border" | 96 | return ports.SceneWindow{}, ports.SurfaceContent{}, "border" | ||
| 97 | } | 97 | } | ||
| 98 | c := surfaces[pick.ID] | 98 | c := surfaces[pick.ID] | ||
M internal/adapters/drm/overlay_test.go +1 -5
| @@ -119,11 +119,7 @@ func TestOverlayPopupAndBorder(t *testing.T) { | |||
| 119 | 119 | t.Fatalf("popup: %q", reason) | |
| 120 | 120 | } | |
| 121 | 121 | s, _ = overlayScene() | |
| 122 | - | s.Border.Width = 2 | |
| 123 | - | if _, _, reason := overlayCandidate(s, c); reason != "" { | |
| 124 | - | t.Fatalf("lone window: %q", reason) | |
| 125 | - | } | |
| 126 | - | s.Windows[len(s.Windows)-1].Neighbors = ports.SideLeft | |
| 122 | + | s.Separators = []ports.Separator{{Rect: ports.Rect{W: 2, H: 2}}} | |
| 127 | 123 | if _, _, reason := overlayCandidate(s, c); reason != "border" { | |
| 128 | 124 | t.Fatalf("border: %q", reason) | |
| 129 | 125 | } | |
| @@ -119,11 +119,7 @@ func TestOverlayPopupAndBorder(t *testing.T) { | |||||
| 119 | t.Fatalf("popup: %q", reason) | 119 | t.Fatalf("popup: %q", reason) | ||
| 120 | } | 120 | } | ||
| 121 | s, _ = overlayScene() | 121 | s, _ = overlayScene() | ||
| 122 | - | s.Border.Width = 2 | 122 | + | s.Separators = []ports.Separator{{Rect: ports.Rect{W: 2, H: 2}}} |
| 123 | - | if _, _, reason := overlayCandidate(s, c); reason != "" { | |||
| 124 | - | t.Fatalf("lone window: %q", reason) | |||
| 125 | - | } | |||
| 126 | - | s.Windows[len(s.Windows)-1].Neighbors = ports.SideLeft | |||
| 127 | if _, _, reason := overlayCandidate(s, c); reason != "border" { | 123 | if _, _, reason := overlayCandidate(s, c); reason != "border" { | ||
| 128 | t.Fatalf("border: %q", reason) | 124 | t.Fatalf("border: %q", reason) | ||
| 129 | } | 125 | } | ||
M internal/adapters/vulkan/renderer.go +7 -99
| @@ -422,7 +422,6 @@ func (r *Renderer) draws(s ports.Scene, contents map[ports.WindowID]ports.Surfac | |||
| 422 | 422 | } | |
| 423 | 423 | } | |
| 424 | 424 | addLayers(false) | |
| 425 | - | var focused *ports.SceneWindow | |
| 426 | 425 | for _, w := range s.Windows { | |
| 427 | 426 | if w.Hidden || w.Rect.W <= 0 || w.Rect.H <= 0 { | |
| 428 | 427 | continue | |
| @@ -447,45 +446,15 @@ func (r *Renderer) draws(s ports.Scene, contents map[ports.WindowID]ports.Surfac | |||
| 447 | 446 | add(body, parseColor(s.Background)) | |
| 448 | 447 | place(w.ID, &content, cx, cy, cw, ch) | |
| 449 | 448 | } | |
| 450 | - | if s.Border.Inactive != "" { | |
| 451 | - | for _, strip := range borderStrips(w.Rect, inset, 0, b) { | |
| 452 | - | add(physRect(strip.X, strip.Y, strip.W, strip.H), parseColor(s.Border.Inactive)) | |
| 453 | - | } | |
| 454 | - | } | |
| 455 | - | if w.Focused && !w.Fullscreen { | |
| 456 | - | focused = &w | |
| 457 | - | } | |
| 458 | 449 | } | |
| 459 | - | // The focused window lights the lines along its own sides only: its | |
| 460 | - | // inset border, and its neighbors' border line where they own it. | |
| 461 | - | // tmux rule (screen-redraw.c): the focused window lights the lines | |
| 462 | - | // along its sides, corners included. With exactly two tiles a single | |
| 463 | - | // line splits them: each lights only its half of it (left/top window: | |
| 464 | - | // first half). | |
| 465 | - | if focused != nil && s.Border.Active != "" && s.Border.Width > 0 { | |
| 466 | - | f, tiles := focused, 0 | |
| 467 | - | for _, w := range s.Windows { | |
| 468 | - | if !w.Hidden && !w.Popup && w.Neighbors != 0 { | |
| 469 | - | tiles++ | |
| 470 | - | } | |
| 471 | - | } | |
| 472 | - | // Inset sides with no neighbor (floats) light fully. | |
| 473 | - | for _, strip := range borderStrips(f.Rect, f.Inset&^f.Neighbors, 0, s.Border.Width) { | |
| 474 | - | add(physRect(strip.X, strip.Y, strip.W, strip.H), parseColor(s.Border.Active)) | |
| 450 | + | // Separators are drawn in order: active lines over inactive ones. | |
| 451 | + | for _, sep := range s.Separators { | |
| 452 | + | col := s.Border.Inactive | |
| 453 | + | if sep.Active { | |
| 454 | + | col = s.Border.Active | |
| 475 | 455 | } | |
| 476 | - | for _, side := range []ports.Sides{ports.SideLeft, ports.SideRight, ports.SideTop, ports.SideBottom} { | |
| 477 | - | if f.Neighbors&side == 0 { | |
| 478 | - | continue | |
| 479 | - | } | |
| 480 | - | out := s.Border.Width | |
| 481 | - | if f.Inset&side != 0 { | |
| 482 | - | out = 0 | |
| 483 | - | } | |
| 484 | - | line := closeCorners(borderStrips(f.Rect, side, out, s.Border.Width)[0], *f, side, s.Border.Width) | |
| 485 | - | if tiles == 2 { | |
| 486 | - | line = halfStrip(line, side) | |
| 487 | - | } | |
| 488 | - | add(physRect(line.X, line.Y, line.W, line.H), parseColor(s.Border.Active)) | |
| 456 | + | if col != "" { | |
| 457 | + | add(physRect(sep.Rect.X, sep.Rect.Y, sep.Rect.W, sep.Rect.H), parseColor(col)) | |
| 489 | 458 | } | |
| 490 | 459 | } | |
| 491 | 460 | // Popups draw only what the client drew, shadows clipped. Window popups | |
| @@ -619,64 +588,3 @@ func (r *Renderer) Close() { | |||
| 619 | 588 | r.instance = 0 | |
| 620 | 589 | } | |
| 621 | 590 | } | |
| 622 | - | ||
| 623 | - | // borderStrips are the b-wide strips along sides of r, in logical pixels: | |
| 624 | - | // inside r when out is 0, else just outside it (a neighbor's border). | |
| 625 | - | func borderStrips(r ports.Rect, sides ports.Sides, out, b int) []ports.Rect { | |
| 626 | - | if b <= 0 { | |
| 627 | - | return nil | |
| 628 | - | } | |
| 629 | - | b = min(b, max(r.W/2, out), max(r.H/2, out)) | |
| 630 | - | var strips []ports.Rect | |
| 631 | - | if sides&ports.SideLeft != 0 { | |
| 632 | - | strips = append(strips, ports.Rect{X: r.X - out, Y: r.Y, W: b, H: r.H}) | |
| 633 | - | } | |
| 634 | - | if sides&ports.SideRight != 0 { | |
| 635 | - | strips = append(strips, ports.Rect{X: r.X + r.W - b + out, Y: r.Y, W: b, H: r.H}) | |
| 636 | - | } | |
| 637 | - | if sides&ports.SideTop != 0 { | |
| 638 | - | strips = append(strips, ports.Rect{X: r.X, Y: r.Y - out, W: r.W, H: b}) | |
| 639 | - | } | |
| 640 | - | if sides&ports.SideBottom != 0 { | |
| 641 | - | strips = append(strips, ports.Rect{X: r.X, Y: r.Y + r.H - b + out, W: r.W, H: b}) | |
| 642 | - | } | |
| 643 | - | return strips | |
| 644 | - | } | |
| 645 | - | ||
| 646 | - | // closeCorners runs a line along side of w b further at each end where | |
| 647 | - | // the crossing line lies outside w (a neighbor's), so lit lines meet. | |
| 648 | - | func closeCorners(line ports.Rect, w ports.SceneWindow, side ports.Sides, b int) ports.Rect { | |
| 649 | - | outside := func(s ports.Sides) bool { return w.Neighbors&s != 0 && w.Inset&s == 0 } | |
| 650 | - | if side == ports.SideLeft || side == ports.SideRight { | |
| 651 | - | if outside(ports.SideTop) { | |
| 652 | - | line.Y, line.H = line.Y-b, line.H+b | |
| 653 | - | } | |
| 654 | - | if outside(ports.SideBottom) { | |
| 655 | - | line.H += b | |
| 656 | - | } | |
| 657 | - | return line | |
| 658 | - | } | |
| 659 | - | if outside(ports.SideLeft) { | |
| 660 | - | line.X, line.W = line.X-b, line.W+b | |
| 661 | - | } | |
| 662 | - | if outside(ports.SideRight) { | |
| 663 | - | line.W += b | |
| 664 | - | } | |
| 665 | - | return line | |
| 666 | - | } | |
| 667 | - | ||
| 668 | - | // halfStrip keeps the half of a separator strip owned by the window on | |
| 669 | - | // side of it: the first half for the left or top window, else the second. | |
| 670 | - | func halfStrip(r ports.Rect, side ports.Sides) ports.Rect { | |
| 671 | - | switch side { | |
| 672 | - | case ports.SideRight: | |
| 673 | - | r.H /= 2 | |
| 674 | - | case ports.SideLeft: | |
| 675 | - | r.Y, r.H = r.Y+r.H/2, r.H-r.H/2 | |
| 676 | - | case ports.SideBottom: | |
| 677 | - | r.W /= 2 | |
| 678 | - | case ports.SideTop: | |
| 679 | - | r.X, r.W = r.X+r.W/2, r.W-r.W/2 | |
| 680 | - | } | |
| 681 | - | return r | |
| 682 | - | } | |
| @@ -422,7 +422,6 @@ func (r *Renderer) draws(s ports.Scene, contents map[ports.WindowID]ports.Surfac | |||||
| 422 | } | 422 | } | ||
| 423 | } | 423 | } | ||
| 424 | addLayers(false) | 424 | addLayers(false) | ||
| 425 | - | var focused *ports.SceneWindow | |||
| 426 | for _, w := range s.Windows { | 425 | for _, w := range s.Windows { | ||
| 427 | if w.Hidden || w.Rect.W <= 0 || w.Rect.H <= 0 { | 426 | if w.Hidden || w.Rect.W <= 0 || w.Rect.H <= 0 { | ||
| 428 | continue | 427 | continue | ||
| @@ -447,45 +446,15 @@ func (r *Renderer) draws(s ports.Scene, contents map[ports.WindowID]ports.Surfac | |||||
| 447 | add(body, parseColor(s.Background)) | 446 | add(body, parseColor(s.Background)) | ||
| 448 | place(w.ID, &content, cx, cy, cw, ch) | 447 | place(w.ID, &content, cx, cy, cw, ch) | ||
| 449 | } | 448 | } | ||
| 450 | - | if s.Border.Inactive != "" { | |||
| 451 | - | for _, strip := range borderStrips(w.Rect, inset, 0, b) { | |||
| 452 | - | add(physRect(strip.X, strip.Y, strip.W, strip.H), parseColor(s.Border.Inactive)) | |||
| 453 | - | } | |||
| 454 | - | } | |||
| 455 | - | if w.Focused && !w.Fullscreen { | |||
| 456 | - | focused = &w | |||
| 457 | - | } | |||
| 458 | } | 449 | } | ||
| 459 | - | // The focused window lights the lines along its own sides only: its | 450 | + | // Separators are drawn in order: active lines over inactive ones. |
| 460 | - | // inset border, and its neighbors' border line where they own it. | 451 | + | for _, sep := range s.Separators { |
| 461 | - | // tmux rule (screen-redraw.c): the focused window lights the lines | 452 | + | col := s.Border.Inactive |
| 462 | - | // along its sides, corners included. With exactly two tiles a single | 453 | + | if sep.Active { |
| 463 | - | // line splits them: each lights only its half of it (left/top window: | 454 | + | col = s.Border.Active |
| 464 | - | // first half). | |||
| 465 | - | if focused != nil && s.Border.Active != "" && s.Border.Width > 0 { | |||
| 466 | - | f, tiles := focused, 0 | |||
| 467 | - | for _, w := range s.Windows { | |||
| 468 | - | if !w.Hidden && !w.Popup && w.Neighbors != 0 { | |||
| 469 | - | tiles++ | |||
| 470 | - | } | |||
| 471 | - | } | |||
| 472 | - | // Inset sides with no neighbor (floats) light fully. | |||
| 473 | - | for _, strip := range borderStrips(f.Rect, f.Inset&^f.Neighbors, 0, s.Border.Width) { | |||
| 474 | - | add(physRect(strip.X, strip.Y, strip.W, strip.H), parseColor(s.Border.Active)) | |||
| 475 | } | 455 | } | ||
| 476 | - | for _, side := range []ports.Sides{ports.SideLeft, ports.SideRight, ports.SideTop, ports.SideBottom} { | 456 | + | if col != "" { |
| 477 | - | if f.Neighbors&side == 0 { | 457 | + | add(physRect(sep.Rect.X, sep.Rect.Y, sep.Rect.W, sep.Rect.H), parseColor(col)) |
| 478 | - | continue | |||
| 479 | - | } | |||
| 480 | - | out := s.Border.Width | |||
| 481 | - | if f.Inset&side != 0 { | |||
| 482 | - | out = 0 | |||
| 483 | - | } | |||
| 484 | - | line := closeCorners(borderStrips(f.Rect, side, out, s.Border.Width)[0], *f, side, s.Border.Width) | |||
| 485 | - | if tiles == 2 { | |||
| 486 | - | line = halfStrip(line, side) | |||
| 487 | - | } | |||
| 488 | - | add(physRect(line.X, line.Y, line.W, line.H), parseColor(s.Border.Active)) | |||
| 489 | } | 458 | } | ||
| 490 | } | 459 | } | ||
| 491 | // Popups draw only what the client drew, shadows clipped. Window popups | 460 | // Popups draw only what the client drew, shadows clipped. Window popups | ||
| @@ -619,64 +588,3 @@ func (r *Renderer) Close() { | |||||
| 619 | r.instance = 0 | 588 | r.instance = 0 | ||
| 620 | } | 589 | } | ||
| 621 | } | 590 | } | ||
| 622 | - | ||||
| 623 | - | // borderStrips are the b-wide strips along sides of r, in logical pixels: | |||
| 624 | - | // inside r when out is 0, else just outside it (a neighbor's border). | |||
| 625 | - | func borderStrips(r ports.Rect, sides ports.Sides, out, b int) []ports.Rect { | |||
| 626 | - | if b <= 0 { | |||
| 627 | - | return nil | |||
| 628 | - | } | |||
| 629 | - | b = min(b, max(r.W/2, out), max(r.H/2, out)) | |||
| 630 | - | var strips []ports.Rect | |||
| 631 | - | if sides&ports.SideLeft != 0 { | |||
| 632 | - | strips = append(strips, ports.Rect{X: r.X - out, Y: r.Y, W: b, H: r.H}) | |||
| 633 | - | } | |||
| 634 | - | if sides&ports.SideRight != 0 { | |||
| 635 | - | strips = append(strips, ports.Rect{X: r.X + r.W - b + out, Y: r.Y, W: b, H: r.H}) | |||
| 636 | - | } | |||
| 637 | - | if sides&ports.SideTop != 0 { | |||
| 638 | - | strips = append(strips, ports.Rect{X: r.X, Y: r.Y - out, W: r.W, H: b}) | |||
| 639 | - | } | |||
| 640 | - | if sides&ports.SideBottom != 0 { | |||
| 641 | - | strips = append(strips, ports.Rect{X: r.X, Y: r.Y + r.H - b + out, W: r.W, H: b}) | |||
| 642 | - | } | |||
| 643 | - | return strips | |||
| 644 | - | } | |||
| 645 | - | ||||
| 646 | - | // closeCorners runs a line along side of w b further at each end where | |||
| 647 | - | // the crossing line lies outside w (a neighbor's), so lit lines meet. | |||
| 648 | - | func closeCorners(line ports.Rect, w ports.SceneWindow, side ports.Sides, b int) ports.Rect { | |||
| 649 | - | outside := func(s ports.Sides) bool { return w.Neighbors&s != 0 && w.Inset&s == 0 } | |||
| 650 | - | if side == ports.SideLeft || side == ports.SideRight { | |||
| 651 | - | if outside(ports.SideTop) { | |||
| 652 | - | line.Y, line.H = line.Y-b, line.H+b | |||
| 653 | - | } | |||
| 654 | - | if outside(ports.SideBottom) { | |||
| 655 | - | line.H += b | |||
| 656 | - | } | |||
| 657 | - | return line | |||
| 658 | - | } | |||
| 659 | - | if outside(ports.SideLeft) { | |||
| 660 | - | line.X, line.W = line.X-b, line.W+b | |||
| 661 | - | } | |||
| 662 | - | if outside(ports.SideRight) { | |||
| 663 | - | line.W += b | |||
| 664 | - | } | |||
| 665 | - | return line | |||
| 666 | - | } | |||
| 667 | - | ||||
| 668 | - | // halfStrip keeps the half of a separator strip owned by the window on | |||
| 669 | - | // side of it: the first half for the left or top window, else the second. | |||
| 670 | - | func halfStrip(r ports.Rect, side ports.Sides) ports.Rect { | |||
| 671 | - | switch side { | |||
| 672 | - | case ports.SideRight: | |||
| 673 | - | r.H /= 2 | |||
| 674 | - | case ports.SideLeft: | |||
| 675 | - | r.Y, r.H = r.Y+r.H/2, r.H-r.H/2 | |||
| 676 | - | case ports.SideBottom: | |||
| 677 | - | r.W /= 2 | |||
| 678 | - | case ports.SideTop: | |||
| 679 | - | r.X, r.W = r.X+r.W/2, r.W-r.W/2 | |||
| 680 | - | } | |||
| 681 | - | return r | |||
| 682 | - | } | |||
M internal/adapters/vulkan/renderer_test.go +13 -37
| @@ -57,47 +57,22 @@ func TestRendererRender(t *testing.T) { | |||
| 57 | 57 | } | |
| 58 | 58 | } | |
| 59 | 59 | } | |
| 60 | + | // The client sits inside its inset sides; separators are drawn over | |
| 61 | + | // the windows in order, with the border colors. | |
| 60 | 62 | win := ports.SceneWindow{ID: 1, Rect: ports.Rect{X: 8, Y: 8, W: 16, H: 16}, Inset: ports.SideAll} | |
| 61 | 63 | c := windowColor(1) | |
| 62 | 64 | wc := color.RGBA{c[0], c[1], c[2], 255} | |
| 63 | - | s := ports.Scene{Background: "#102030", Border: ports.Border{Width: 2, Active: "#b4befe", Inactive: "#313244"}, Windows: []ports.SceneWindow{win}} | |
| 64 | - | check(s, map[image.Point]color.RGBA{{0, 0}: bg, {9, 9}: {0x31, 0x32, 0x44, 255}, {10, 10}: wc, {16, 16}: wc, {21, 21}: wc, {22, 21}: {0x31, 0x32, 0x44, 255}, {21, 22}: {0x31, 0x32, 0x44, 255}, {23, 23}: {0x31, 0x32, 0x44, 255}, {24, 24}: bg}) | |
| 65 | - | s.Windows[0].Focused = true | |
| 66 | - | check(s, map[image.Point]color.RGBA{{9, 9}: {0xb4, 0xbe, 0xfe, 255}, {10, 10}: wc, {16, 16}: wc}) | |
| 67 | - | // tmux style: the left window owns the shared line. With two tiles the | |
| 68 | - | // focused one lights its half: top for the left window, bottom for the right. | |
| 69 | 65 | gray, lit := color.RGBA{0x31, 0x32, 0x44, 255}, color.RGBA{0xb4, 0xbe, 0xfe, 255} | |
| 70 | - | pair := s | |
| 71 | - | pair.Windows = []ports.SceneWindow{ | |
| 72 | - | {ID: 1, Rect: ports.Rect{X: 0, Y: 0, W: 16, H: 16}, Inset: ports.SideRight, Neighbors: ports.SideRight}, | |
| 73 | - | {ID: 2, Rect: ports.Rect{X: 16, Y: 0, W: 16, H: 16}, Neighbors: ports.SideLeft, Focused: true}, | |
| 74 | - | } | |
| 75 | - | check(pair, map[image.Point]color.RGBA{{14, 12}: lit, {15, 12}: lit, {14, 3}: gray, {13, 5}: wc, {0, 0}: wc}) | |
| 76 | - | pair.Windows[1].Focused, pair.Windows[0].Focused = false, true | |
| 77 | - | check(pair, map[image.Point]color.RGBA{{14, 3}: lit, {14, 12}: gray}) | |
| 78 | - | pair.Windows[0].Focused = false | |
| 79 | - | check(pair, map[image.Point]color.RGBA{{14, 5}: gray, {15, 5}: gray}) | |
| 80 | - | // Three columns (tmux rule): the focused window lights its full lines. | |
| 81 | - | three := s | |
| 82 | - | three.Windows = []ports.SceneWindow{ | |
| 83 | - | {ID: 1, Rect: ports.Rect{X: 0, Y: 0, W: 16, H: 16}, Inset: ports.SideRight, Neighbors: ports.SideRight}, | |
| 84 | - | {ID: 2, Rect: ports.Rect{X: 16, Y: 0, W: 16, H: 16}, Inset: ports.SideRight, Neighbors: ports.SideLeft | ports.SideRight, Focused: true}, | |
| 85 | - | {ID: 3, Rect: ports.Rect{X: 32, Y: 0, W: 16, H: 16}, Neighbors: ports.SideLeft}, | |
| 86 | - | } | |
| 87 | - | check(three, map[image.Point]color.RGBA{{14, 3}: lit, {14, 12}: lit, {30, 3}: lit, {30, 12}: lit}) | |
| 88 | - | three.Windows[1].Focused, three.Windows[0].Focused = false, true | |
| 89 | - | check(three, map[image.Point]color.RGBA{{14, 3}: lit, {14, 12}: lit, {30, 3}: gray, {30, 12}: gray}) | |
| 90 | - | // Bottom-right tile of a split column: its left line and top line are | |
| 91 | - | // lit, and they meet in the corner. | |
| 92 | - | grid := s | |
| 93 | - | grid.Windows = []ports.SceneWindow{ | |
| 94 | - | {ID: 1, Rect: ports.Rect{X: 0, Y: 0, W: 16, H: 32}, Inset: ports.SideRight, Neighbors: ports.SideRight}, | |
| 95 | - | {ID: 2, Rect: ports.Rect{X: 16, Y: 0, W: 16, H: 16}, Inset: ports.SideBottom, Neighbors: ports.SideLeft | ports.SideBottom}, | |
| 96 | - | {ID: 3, Rect: ports.Rect{X: 16, Y: 16, W: 16, H: 16}, Neighbors: ports.SideLeft | ports.SideTop, Focused: true}, | |
| 97 | - | } | |
| 98 | - | check(grid, map[image.Point]color.RGBA{{14, 30}: lit, {14, 20}: lit, {14, 14}: lit, {14, 5}: gray, {24, 14}: lit, {17, 14}: lit}) | |
| 99 | - | grid.Windows[2].Focused, grid.Windows[1].Focused = false, true | |
| 100 | - | check(grid, map[image.Point]color.RGBA{{14, 3}: lit, {14, 14}: lit, {14, 15}: lit, {14, 20}: gray, {30, 14}: lit}) | |
| 66 | + | s := ports.Scene{Background: "#102030", Border: ports.Border{Width: 2, Active: "#b4befe", Inactive: "#313244"}, Windows: []ports.SceneWindow{win}} | |
| 67 | + | check(s, map[image.Point]color.RGBA{{0, 0}: bg, {9, 9}: bg, {10, 10}: wc, {16, 16}: wc, {21, 21}: wc, {22, 22}: bg, {24, 24}: bg}) | |
| 68 | + | s.Separators = []ports.Separator{ | |
| 69 | + | {Rect: ports.Rect{X: 8, Y: 8, W: 16, H: 2}}, | |
| 70 | + | {Rect: ports.Rect{X: 8, Y: 8, W: 8, H: 2}, Active: true}, | |
| 71 | + | } | |
| 72 | + | check(s, map[image.Point]color.RGBA{{9, 9}: lit, {15, 9}: lit, {16, 9}: gray, {23, 9}: gray, {10, 10}: wc}) | |
| 73 | + | s.Border.Inactive = "" | |
| 74 | + | check(s, map[image.Point]color.RGBA{{9, 9}: lit, {16, 9}: bg}) | |
| 75 | + | s.Separators, s.Border.Inactive = nil, "#313244" | |
| 101 | 76 | s.Windows[0].Fullscreen = true | |
| 102 | 77 | check(s, map[image.Point]color.RGBA{{9, 9}: wc}) | |
| 103 | 78 | s.Windows[0].Fullscreen = false | |
| @@ -170,6 +145,7 @@ func TestRendererUploadOrderAndOpaque(t *testing.T) { | |||
| 170 | 145 | } | |
| 171 | 146 | scene.Border = ports.Border{Width: 4, Active: "#ffffff"} | |
| 172 | 147 | scene.Windows = []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 2, Y: 2, W: 16, H: 16}, Focused: true, Inset: ports.SideAll}} | |
| 148 | + | scene.Separators = []ports.Separator{{Rect: ports.Rect{X: 2, Y: 2, W: 16, H: 4}, Active: true}} | |
| 173 | 149 | px := make([]byte, 16*16*4) | |
| 174 | 150 | for i := 0; i < len(px); i += 4 { | |
| 175 | 151 | copy(px[i:i+4], []byte{3, 5, 7, 0}) | |
| @@ -57,47 +57,22 @@ func TestRendererRender(t *testing.T) { | |||||
| 57 | } | 57 | } | ||
| 58 | } | 58 | } | ||
| 59 | } | 59 | } | ||
| 60 | + | // The client sits inside its inset sides; separators are drawn over | |||
| 61 | + | // the windows in order, with the border colors. | |||
| 60 | win := ports.SceneWindow{ID: 1, Rect: ports.Rect{X: 8, Y: 8, W: 16, H: 16}, Inset: ports.SideAll} | 62 | win := ports.SceneWindow{ID: 1, Rect: ports.Rect{X: 8, Y: 8, W: 16, H: 16}, Inset: ports.SideAll} | ||
| 61 | c := windowColor(1) | 63 | c := windowColor(1) | ||
| 62 | wc := color.RGBA{c[0], c[1], c[2], 255} | 64 | wc := color.RGBA{c[0], c[1], c[2], 255} | ||
| 63 | - | s := ports.Scene{Background: "#102030", Border: ports.Border{Width: 2, Active: "#b4befe", Inactive: "#313244"}, Windows: []ports.SceneWindow{win}} | |||
| 64 | - | check(s, map[image.Point]color.RGBA{{0, 0}: bg, {9, 9}: {0x31, 0x32, 0x44, 255}, {10, 10}: wc, {16, 16}: wc, {21, 21}: wc, {22, 21}: {0x31, 0x32, 0x44, 255}, {21, 22}: {0x31, 0x32, 0x44, 255}, {23, 23}: {0x31, 0x32, 0x44, 255}, {24, 24}: bg}) | |||
| 65 | - | s.Windows[0].Focused = true | |||
| 66 | - | check(s, map[image.Point]color.RGBA{{9, 9}: {0xb4, 0xbe, 0xfe, 255}, {10, 10}: wc, {16, 16}: wc}) | |||
| 67 | - | // tmux style: the left window owns the shared line. With two tiles the | |||
| 68 | - | // focused one lights its half: top for the left window, bottom for the right. | |||
| 69 | gray, lit := color.RGBA{0x31, 0x32, 0x44, 255}, color.RGBA{0xb4, 0xbe, 0xfe, 255} | 65 | gray, lit := color.RGBA{0x31, 0x32, 0x44, 255}, color.RGBA{0xb4, 0xbe, 0xfe, 255} | ||
| 70 | - | pair := s | 66 | + | s := ports.Scene{Background: "#102030", Border: ports.Border{Width: 2, Active: "#b4befe", Inactive: "#313244"}, Windows: []ports.SceneWindow{win}} |
| 71 | - | pair.Windows = []ports.SceneWindow{ | 67 | + | check(s, map[image.Point]color.RGBA{{0, 0}: bg, {9, 9}: bg, {10, 10}: wc, {16, 16}: wc, {21, 21}: wc, {22, 22}: bg, {24, 24}: bg}) |
| 72 | - | {ID: 1, Rect: ports.Rect{X: 0, Y: 0, W: 16, H: 16}, Inset: ports.SideRight, Neighbors: ports.SideRight}, | 68 | + | s.Separators = []ports.Separator{ |
| 73 | - | {ID: 2, Rect: ports.Rect{X: 16, Y: 0, W: 16, H: 16}, Neighbors: ports.SideLeft, Focused: true}, | 69 | + | {Rect: ports.Rect{X: 8, Y: 8, W: 16, H: 2}}, |
| 74 | - | } | 70 | + | {Rect: ports.Rect{X: 8, Y: 8, W: 8, H: 2}, Active: true}, |
| 75 | - | check(pair, map[image.Point]color.RGBA{{14, 12}: lit, {15, 12}: lit, {14, 3}: gray, {13, 5}: wc, {0, 0}: wc}) | 71 | + | } |
| 76 | - | pair.Windows[1].Focused, pair.Windows[0].Focused = false, true | 72 | + | check(s, map[image.Point]color.RGBA{{9, 9}: lit, {15, 9}: lit, {16, 9}: gray, {23, 9}: gray, {10, 10}: wc}) |
| 77 | - | check(pair, map[image.Point]color.RGBA{{14, 3}: lit, {14, 12}: gray}) | 73 | + | s.Border.Inactive = "" |
| 78 | - | pair.Windows[0].Focused = false | 74 | + | check(s, map[image.Point]color.RGBA{{9, 9}: lit, {16, 9}: bg}) |
| 79 | - | check(pair, map[image.Point]color.RGBA{{14, 5}: gray, {15, 5}: gray}) | 75 | + | s.Separators, s.Border.Inactive = nil, "#313244" |
| 80 | - | // Three columns (tmux rule): the focused window lights its full lines. | |||
| 81 | - | three := s | |||
| 82 | - | three.Windows = []ports.SceneWindow{ | |||
| 83 | - | {ID: 1, Rect: ports.Rect{X: 0, Y: 0, W: 16, H: 16}, Inset: ports.SideRight, Neighbors: ports.SideRight}, | |||
| 84 | - | {ID: 2, Rect: ports.Rect{X: 16, Y: 0, W: 16, H: 16}, Inset: ports.SideRight, Neighbors: ports.SideLeft | ports.SideRight, Focused: true}, | |||
| 85 | - | {ID: 3, Rect: ports.Rect{X: 32, Y: 0, W: 16, H: 16}, Neighbors: ports.SideLeft}, | |||
| 86 | - | } | |||
| 87 | - | check(three, map[image.Point]color.RGBA{{14, 3}: lit, {14, 12}: lit, {30, 3}: lit, {30, 12}: lit}) | |||
| 88 | - | three.Windows[1].Focused, three.Windows[0].Focused = false, true | |||
| 89 | - | check(three, map[image.Point]color.RGBA{{14, 3}: lit, {14, 12}: lit, {30, 3}: gray, {30, 12}: gray}) | |||
| 90 | - | // Bottom-right tile of a split column: its left line and top line are | |||
| 91 | - | // lit, and they meet in the corner. | |||
| 92 | - | grid := s | |||
| 93 | - | grid.Windows = []ports.SceneWindow{ | |||
| 94 | - | {ID: 1, Rect: ports.Rect{X: 0, Y: 0, W: 16, H: 32}, Inset: ports.SideRight, Neighbors: ports.SideRight}, | |||
| 95 | - | {ID: 2, Rect: ports.Rect{X: 16, Y: 0, W: 16, H: 16}, Inset: ports.SideBottom, Neighbors: ports.SideLeft | ports.SideBottom}, | |||
| 96 | - | {ID: 3, Rect: ports.Rect{X: 16, Y: 16, W: 16, H: 16}, Neighbors: ports.SideLeft | ports.SideTop, Focused: true}, | |||
| 97 | - | } | |||
| 98 | - | check(grid, map[image.Point]color.RGBA{{14, 30}: lit, {14, 20}: lit, {14, 14}: lit, {14, 5}: gray, {24, 14}: lit, {17, 14}: lit}) | |||
| 99 | - | grid.Windows[2].Focused, grid.Windows[1].Focused = false, true | |||
| 100 | - | check(grid, map[image.Point]color.RGBA{{14, 3}: lit, {14, 14}: lit, {14, 15}: lit, {14, 20}: gray, {30, 14}: lit}) | |||
| 101 | s.Windows[0].Fullscreen = true | 76 | s.Windows[0].Fullscreen = true | ||
| 102 | check(s, map[image.Point]color.RGBA{{9, 9}: wc}) | 77 | check(s, map[image.Point]color.RGBA{{9, 9}: wc}) | ||
| 103 | s.Windows[0].Fullscreen = false | 78 | s.Windows[0].Fullscreen = false | ||
| @@ -170,6 +145,7 @@ func TestRendererUploadOrderAndOpaque(t *testing.T) { | |||||
| 170 | } | 145 | } | ||
| 171 | scene.Border = ports.Border{Width: 4, Active: "#ffffff"} | 146 | scene.Border = ports.Border{Width: 4, Active: "#ffffff"} | ||
| 172 | scene.Windows = []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 2, Y: 2, W: 16, H: 16}, Focused: true, Inset: ports.SideAll}} | 147 | scene.Windows = []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 2, Y: 2, W: 16, H: 16}, Focused: true, Inset: ports.SideAll}} | ||
| 148 | + | scene.Separators = []ports.Separator{{Rect: ports.Rect{X: 2, Y: 2, W: 16, H: 4}, Active: true}} | |||
| 173 | px := make([]byte, 16*16*4) | 149 | px := make([]byte, 16*16*4) | ||
| 174 | for i := 0; i < len(px); i += 4 { | 150 | for i := 0; i < len(px); i += 4 { | ||
| 175 | copy(px[i:i+4], []byte{3, 5, 7, 0}) | 151 | copy(px[i:i+4], []byte{3, 5, 7, 0}) | ||
M internal/core/core.go +5 -2
| @@ -399,11 +399,14 @@ func (c *Core) publish(ctx context.Context) error { | |||
| 399 | 399 | c.seq++ | |
| 400 | 400 | o := sc.mon.Output() | |
| 401 | 401 | scene := ports.Scene{Output: sc.name(), Seq: c.seq, OutputWidth: o.W, OutputHeight: o.H, Scale: sc.scale, Background: c.cfg.Background.Color, Border: ports.Border{Width: c.cfg.Border.Width, Active: c.cfg.Border.Active, Inactive: c.cfg.Border.Inactive}, Windows: make([]ports.SceneWindow, 0), Layers: append([]ports.SceneLayer(nil), sc.placed...)} | |
| 402 | - | for _, p := range sc.mon.Layout() { | |
| 402 | + | layout := sc.mon.Layout() | |
| 403 | + | // Only the focused output lights the focused window's lines. | |
| 404 | + | scene.Separators = separators(layout, c.cfg.Border.Width, i == c.focusScreen) | |
| 405 | + | for _, p := range layout { | |
| 403 | 406 | alive[p.ID] = true | |
| 404 | 407 | // Only the focused output shows the focused border. | |
| 405 | 408 | focused := p.Focused && i == c.focusScreen | |
| 406 | - | scene.Windows = append(scene.Windows, ports.SceneWindow{ID: p.ID, Rect: p.Rect, Focused: focused, Fullscreen: p.Fullscreen, Hidden: p.Hidden, Neighbors: p.Neighbors, Inset: p.Inset}) | |
| 409 | + | scene.Windows = append(scene.Windows, ports.SceneWindow{ID: p.ID, Rect: p.Rect, Focused: focused, Fullscreen: p.Fullscreen, Hidden: p.Hidden, Inset: p.Inset}) | |
| 407 | 410 | floating := p.Floating | |
| 408 | 411 | if p.Hidden { | |
| 409 | 412 | if old, ok := c.sent[p.ID]; ok && (old.Activated || old.Output != "") { | |
| @@ -399,11 +399,14 @@ func (c *Core) publish(ctx context.Context) error { | |||||
| 399 | c.seq++ | 399 | c.seq++ | ||
| 400 | o := sc.mon.Output() | 400 | o := sc.mon.Output() | ||
| 401 | scene := ports.Scene{Output: sc.name(), Seq: c.seq, OutputWidth: o.W, OutputHeight: o.H, Scale: sc.scale, Background: c.cfg.Background.Color, Border: ports.Border{Width: c.cfg.Border.Width, Active: c.cfg.Border.Active, Inactive: c.cfg.Border.Inactive}, Windows: make([]ports.SceneWindow, 0), Layers: append([]ports.SceneLayer(nil), sc.placed...)} | 401 | scene := ports.Scene{Output: sc.name(), Seq: c.seq, OutputWidth: o.W, OutputHeight: o.H, Scale: sc.scale, Background: c.cfg.Background.Color, Border: ports.Border{Width: c.cfg.Border.Width, Active: c.cfg.Border.Active, Inactive: c.cfg.Border.Inactive}, Windows: make([]ports.SceneWindow, 0), Layers: append([]ports.SceneLayer(nil), sc.placed...)} | ||
| 402 | - | for _, p := range sc.mon.Layout() { | 402 | + | layout := sc.mon.Layout() |
| 403 | + | // Only the focused output lights the focused window's lines. | |||
| 404 | + | scene.Separators = separators(layout, c.cfg.Border.Width, i == c.focusScreen) | |||
| 405 | + | for _, p := range layout { | |||
| 403 | alive[p.ID] = true | 406 | alive[p.ID] = true | ||
| 404 | // Only the focused output shows the focused border. | 407 | // Only the focused output shows the focused border. | ||
| 405 | focused := p.Focused && i == c.focusScreen | 408 | focused := p.Focused && i == c.focusScreen | ||
| 406 | - | scene.Windows = append(scene.Windows, ports.SceneWindow{ID: p.ID, Rect: p.Rect, Focused: focused, Fullscreen: p.Fullscreen, Hidden: p.Hidden, Neighbors: p.Neighbors, Inset: p.Inset}) | 409 | + | scene.Windows = append(scene.Windows, ports.SceneWindow{ID: p.ID, Rect: p.Rect, Focused: focused, Fullscreen: p.Fullscreen, Hidden: p.Hidden, Inset: p.Inset}) |
| 407 | floating := p.Floating | 410 | floating := p.Floating | ||
| 408 | if p.Hidden { | 411 | if p.Hidden { | ||
| 409 | if old, ok := c.sent[p.ID]; ok && (old.Activated || old.Output != "") { | 412 | if old, ok := c.sent[p.ID]; ok && (old.Activated || old.Output != "") { | ||
M internal/core/core_test.go +1 -1
| @@ -306,7 +306,7 @@ func TestBorderInset(t *testing.T) { | |||
| 306 | 306 | client <- ports.WindowMapped{ID: 2} | |
| 307 | 307 | s := scene(t, scenes) | |
| 308 | 308 | r = s.Windows[1].Rect | |
| 309 | - | if s.Windows[0].Inset != ports.SideRight || s.Windows[1].Inset != 0 || s.Windows[1].Neighbors != ports.SideLeft { | |
| 309 | + | if s.Windows[0].Inset != ports.SideRight || s.Windows[1].Inset != 0 || len(s.Separators) == 0 { | |
| 310 | 310 | t.Fatal(s) | |
| 311 | 311 | } | |
| 312 | 312 | want := ports.ConfigureWindow{ID: 1, Width: s.Windows[0].Rect.W - 2, Height: r.H, Activated: false, Output: "OUT-1"} | |
| @@ -306,7 +306,7 @@ func TestBorderInset(t *testing.T) { | |||||
| 306 | client <- ports.WindowMapped{ID: 2} | 306 | client <- ports.WindowMapped{ID: 2} | ||
| 307 | s := scene(t, scenes) | 307 | s := scene(t, scenes) | ||
| 308 | r = s.Windows[1].Rect | 308 | r = s.Windows[1].Rect | ||
| 309 | - | if s.Windows[0].Inset != ports.SideRight || s.Windows[1].Inset != 0 || s.Windows[1].Neighbors != ports.SideLeft { | 309 | + | if s.Windows[0].Inset != ports.SideRight || s.Windows[1].Inset != 0 || len(s.Separators) == 0 { |
| 310 | t.Fatal(s) | 310 | t.Fatal(s) | ||
| 311 | } | 311 | } | ||
| 312 | want := ports.ConfigureWindow{ID: 1, Width: s.Windows[0].Rect.W - 2, Height: r.H, Activated: false, Output: "OUT-1"} | 312 | want := ports.ConfigureWindow{ID: 1, Width: s.Windows[0].Rect.W - 2, Height: r.H, Activated: false, Output: "OUT-1"} | ||
A internal/core/separators.go +119 -0
| @@ -0,0 +1,119 @@ | |||
| 1 | + | package core | |
| 2 | + | ||
| 3 | + | import "github.com/bnema/nefertty/internal/ports" | |
| 4 | + | ||
| 5 | + | // Separator lines follow tmux (screen-redraw.c): tiles share one line per | |
| 6 | + | // side where they touch, owned by the left or top tile (Placement.Inset). | |
| 7 | + | // The focused tile lights the lines along its sides, corners included. | |
| 8 | + | // With exactly two tiles one line splits them, and each lights only its | |
| 9 | + | // half of it: the left or top tile the first half. Floats get a full | |
| 10 | + | // border of their own. | |
| 11 | + | ||
| 12 | + | var sides = [...]ports.Sides{ports.SideLeft, ports.SideRight, ports.SideTop, ports.SideBottom} | |
| 13 | + | ||
| 14 | + | // separators are the lines of a layout, inactive ones first so active | |
| 15 | + | // ones draw over them. lit says whether the focused tile shows focus | |
| 16 | + | // (only on the focused output). | |
| 17 | + | func separators(ps []Placement, width int, lit bool) []ports.Separator { | |
| 18 | + | if width <= 0 { | |
| 19 | + | return nil | |
| 20 | + | } | |
| 21 | + | var out []ports.Separator | |
| 22 | + | var focused *Placement | |
| 23 | + | tiles := 0 | |
| 24 | + | for i := range ps { | |
| 25 | + | p := &ps[i] | |
| 26 | + | if p.Hidden || p.Fullscreen || p.Rect.W <= 0 || p.Rect.H <= 0 { | |
| 27 | + | continue | |
| 28 | + | } | |
| 29 | + | if p.Neighbors != 0 { | |
| 30 | + | tiles++ | |
| 31 | + | } | |
| 32 | + | for _, s := range sides { | |
| 33 | + | if p.Inset&s != 0 { | |
| 34 | + | out = append(out, ports.Separator{Rect: strip(p.Rect, s, 0, width)}) | |
| 35 | + | } | |
| 36 | + | } | |
| 37 | + | if p.Focused && lit { | |
| 38 | + | focused = p | |
| 39 | + | } | |
| 40 | + | } | |
| 41 | + | if focused == nil { | |
| 42 | + | return out | |
| 43 | + | } | |
| 44 | + | f := focused | |
| 45 | + | for _, s := range sides { | |
| 46 | + | var line ports.Rect | |
| 47 | + | switch { | |
| 48 | + | case f.Neighbors&s != 0 && f.Inset&s != 0: | |
| 49 | + | line = strip(f.Rect, s, 0, width) | |
| 50 | + | case f.Neighbors&s != 0: | |
| 51 | + | line = strip(f.Rect, s, width, width) | |
| 52 | + | case f.Inset&s != 0: | |
| 53 | + | // A float's own border. | |
| 54 | + | out = append(out, ports.Separator{Rect: strip(f.Rect, s, 0, width), Active: true}) | |
| 55 | + | continue | |
| 56 | + | default: | |
| 57 | + | continue | |
| 58 | + | } | |
| 59 | + | line = closeCorners(line, *f, s, width) | |
| 60 | + | if tiles == 2 { | |
| 61 | + | line = half(line, s) | |
| 62 | + | } | |
| 63 | + | out = append(out, ports.Separator{Rect: line, Active: true}) | |
| 64 | + | } | |
| 65 | + | return out | |
| 66 | + | } | |
| 67 | + | ||
| 68 | + | // strip is the b-wide strip along side s of r: inside r when out is 0, | |
| 69 | + | // else just outside it (the neighbor's line). | |
| 70 | + | func strip(r Rect, s ports.Sides, out, b int) Rect { | |
| 71 | + | b = min(b, max(r.W/2, out), max(r.H/2, out)) | |
| 72 | + | switch s { | |
| 73 | + | case ports.SideLeft: | |
| 74 | + | return Rect{X: r.X - out, Y: r.Y, W: b, H: r.H} | |
| 75 | + | case ports.SideRight: | |
| 76 | + | return Rect{X: r.X + r.W - b + out, Y: r.Y, W: b, H: r.H} | |
| 77 | + | case ports.SideTop: | |
| 78 | + | return Rect{X: r.X, Y: r.Y - out, W: r.W, H: b} | |
| 79 | + | } | |
| 80 | + | return Rect{X: r.X, Y: r.Y + r.H - b + out, W: r.W, H: b} | |
| 81 | + | } | |
| 82 | + | ||
| 83 | + | // closeCorners runs a line along side s of p b further at each end where | |
| 84 | + | // the crossing line lies outside p (a neighbor's), so lit lines meet. | |
| 85 | + | func closeCorners(line Rect, p Placement, s ports.Sides, b int) Rect { | |
| 86 | + | outside := func(s ports.Sides) bool { return p.Neighbors&s != 0 && p.Inset&s == 0 } | |
| 87 | + | if s == ports.SideLeft || s == ports.SideRight { | |
| 88 | + | if outside(ports.SideTop) { | |
| 89 | + | line.Y, line.H = line.Y-b, line.H+b | |
| 90 | + | } | |
| 91 | + | if outside(ports.SideBottom) { | |
| 92 | + | line.H += b | |
| 93 | + | } | |
| 94 | + | return line | |
| 95 | + | } | |
| 96 | + | if outside(ports.SideLeft) { | |
| 97 | + | line.X, line.W = line.X-b, line.W+b | |
| 98 | + | } | |
| 99 | + | if outside(ports.SideRight) { | |
| 100 | + | line.W += b | |
| 101 | + | } | |
| 102 | + | return line | |
| 103 | + | } | |
| 104 | + | ||
| 105 | + | // half keeps the half of a line owned by the tile on side s of it: the | |
| 106 | + | // first half for the left or top tile, else the second. | |
| 107 | + | func half(r Rect, s ports.Sides) Rect { | |
| 108 | + | switch s { | |
| 109 | + | case ports.SideRight: | |
| 110 | + | r.H /= 2 | |
| 111 | + | case ports.SideLeft: | |
| 112 | + | r.Y, r.H = r.Y+r.H/2, r.H-r.H/2 | |
| 113 | + | case ports.SideBottom: | |
| 114 | + | r.W /= 2 | |
| 115 | + | case ports.SideTop: | |
| 116 | + | r.X, r.W = r.X+r.W/2, r.W-r.W/2 | |
| 117 | + | } | |
| 118 | + | return r | |
| 119 | + | } | |
| @@ -0,0 +1,119 @@ | |||||
| 1 | + | package core | |||
| 2 | + | ||||
| 3 | + | import "github.com/bnema/nefertty/internal/ports" | |||
| 4 | + | ||||
| 5 | + | // Separator lines follow tmux (screen-redraw.c): tiles share one line per | |||
| 6 | + | // side where they touch, owned by the left or top tile (Placement.Inset). | |||
| 7 | + | // The focused tile lights the lines along its sides, corners included. | |||
| 8 | + | // With exactly two tiles one line splits them, and each lights only its | |||
| 9 | + | // half of it: the left or top tile the first half. Floats get a full | |||
| 10 | + | // border of their own. | |||
| 11 | + | ||||
| 12 | + | var sides = [...]ports.Sides{ports.SideLeft, ports.SideRight, ports.SideTop, ports.SideBottom} | |||
| 13 | + | ||||
| 14 | + | // separators are the lines of a layout, inactive ones first so active | |||
| 15 | + | // ones draw over them. lit says whether the focused tile shows focus | |||
| 16 | + | // (only on the focused output). | |||
| 17 | + | func separators(ps []Placement, width int, lit bool) []ports.Separator { | |||
| 18 | + | if width <= 0 { | |||
| 19 | + | return nil | |||
| 20 | + | } | |||
| 21 | + | var out []ports.Separator | |||
| 22 | + | var focused *Placement | |||
| 23 | + | tiles := 0 | |||
| 24 | + | for i := range ps { | |||
| 25 | + | p := &ps[i] | |||
| 26 | + | if p.Hidden || p.Fullscreen || p.Rect.W <= 0 || p.Rect.H <= 0 { | |||
| 27 | + | continue | |||
| 28 | + | } | |||
| 29 | + | if p.Neighbors != 0 { | |||
| 30 | + | tiles++ | |||
| 31 | + | } | |||
| 32 | + | for _, s := range sides { | |||
| 33 | + | if p.Inset&s != 0 { | |||
| 34 | + | out = append(out, ports.Separator{Rect: strip(p.Rect, s, 0, width)}) | |||
| 35 | + | } | |||
| 36 | + | } | |||
| 37 | + | if p.Focused && lit { | |||
| 38 | + | focused = p | |||
| 39 | + | } | |||
| 40 | + | } | |||
| 41 | + | if focused == nil { | |||
| 42 | + | return out | |||
| 43 | + | } | |||
| 44 | + | f := focused | |||
| 45 | + | for _, s := range sides { | |||
| 46 | + | var line ports.Rect | |||
| 47 | + | switch { | |||
| 48 | + | case f.Neighbors&s != 0 && f.Inset&s != 0: | |||
| 49 | + | line = strip(f.Rect, s, 0, width) | |||
| 50 | + | case f.Neighbors&s != 0: | |||
| 51 | + | line = strip(f.Rect, s, width, width) | |||
| 52 | + | case f.Inset&s != 0: | |||
| 53 | + | // A float's own border. | |||
| 54 | + | out = append(out, ports.Separator{Rect: strip(f.Rect, s, 0, width), Active: true}) | |||
| 55 | + | continue | |||
| 56 | + | default: | |||
| 57 | + | continue | |||
| 58 | + | } | |||
| 59 | + | line = closeCorners(line, *f, s, width) | |||
| 60 | + | if tiles == 2 { | |||
| 61 | + | line = half(line, s) | |||
| 62 | + | } | |||
| 63 | + | out = append(out, ports.Separator{Rect: line, Active: true}) | |||
| 64 | + | } | |||
| 65 | + | return out | |||
| 66 | + | } | |||
| 67 | + | ||||
| 68 | + | // strip is the b-wide strip along side s of r: inside r when out is 0, | |||
| 69 | + | // else just outside it (the neighbor's line). | |||
| 70 | + | func strip(r Rect, s ports.Sides, out, b int) Rect { | |||
| 71 | + | b = min(b, max(r.W/2, out), max(r.H/2, out)) | |||
| 72 | + | switch s { | |||
| 73 | + | case ports.SideLeft: | |||
| 74 | + | return Rect{X: r.X - out, Y: r.Y, W: b, H: r.H} | |||
| 75 | + | case ports.SideRight: | |||
| 76 | + | return Rect{X: r.X + r.W - b + out, Y: r.Y, W: b, H: r.H} | |||
| 77 | + | case ports.SideTop: | |||
| 78 | + | return Rect{X: r.X, Y: r.Y - out, W: r.W, H: b} | |||
| 79 | + | } | |||
| 80 | + | return Rect{X: r.X, Y: r.Y + r.H - b + out, W: r.W, H: b} | |||
| 81 | + | } | |||
| 82 | + | ||||
| 83 | + | // closeCorners runs a line along side s of p b further at each end where | |||
| 84 | + | // the crossing line lies outside p (a neighbor's), so lit lines meet. | |||
| 85 | + | func closeCorners(line Rect, p Placement, s ports.Sides, b int) Rect { | |||
| 86 | + | outside := func(s ports.Sides) bool { return p.Neighbors&s != 0 && p.Inset&s == 0 } | |||
| 87 | + | if s == ports.SideLeft || s == ports.SideRight { | |||
| 88 | + | if outside(ports.SideTop) { | |||
| 89 | + | line.Y, line.H = line.Y-b, line.H+b | |||
| 90 | + | } | |||
| 91 | + | if outside(ports.SideBottom) { | |||
| 92 | + | line.H += b | |||
| 93 | + | } | |||
| 94 | + | return line | |||
| 95 | + | } | |||
| 96 | + | if outside(ports.SideLeft) { | |||
| 97 | + | line.X, line.W = line.X-b, line.W+b | |||
| 98 | + | } | |||
| 99 | + | if outside(ports.SideRight) { | |||
| 100 | + | line.W += b | |||
| 101 | + | } | |||
| 102 | + | return line | |||
| 103 | + | } | |||
| 104 | + | ||||
| 105 | + | // half keeps the half of a line owned by the tile on side s of it: the | |||
| 106 | + | // first half for the left or top tile, else the second. | |||
| 107 | + | func half(r Rect, s ports.Sides) Rect { | |||
| 108 | + | switch s { | |||
| 109 | + | case ports.SideRight: | |||
| 110 | + | r.H /= 2 | |||
| 111 | + | case ports.SideLeft: | |||
| 112 | + | r.Y, r.H = r.Y+r.H/2, r.H-r.H/2 | |||
| 113 | + | case ports.SideBottom: | |||
| 114 | + | r.W /= 2 | |||
| 115 | + | case ports.SideTop: | |||
| 116 | + | r.X, r.W = r.X+r.W/2, r.W-r.W/2 | |||
| 117 | + | } | |||
| 118 | + | return r | |||
| 119 | + | } | |||
A internal/core/separators_test.go +99 -0
| @@ -0,0 +1,99 @@ | |||
| 1 | + | package core | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "testing" | |
| 5 | + | ||
| 6 | + | "github.com/bnema/nefertty/internal/ports" | |
| 7 | + | ) | |
| 8 | + | ||
| 9 | + | // tiles lays out rects edge to edge (no gap) with focus on index f. | |
| 10 | + | func tiles(f int, rs ...Rect) []Placement { | |
| 11 | + | ps := make([]Placement, len(rs)) | |
| 12 | + | for i, r := range rs { | |
| 13 | + | ps[i] = Placement{ID: WindowID(i + 1), Rect: r, Focused: i == f} | |
| 14 | + | } | |
| 15 | + | setNeighbors(ps, 0, Rect{W: 1000, H: 1000}) | |
| 16 | + | return ps | |
| 17 | + | } | |
| 18 | + | ||
| 19 | + | // colorAt is what the renderer shows at (x, y): the last separator over | |
| 20 | + | // it wins. "" is no line. | |
| 21 | + | func colorAt(seps []ports.Separator, x, y int) string { | |
| 22 | + | got := "" | |
| 23 | + | for _, s := range seps { | |
| 24 | + | r := s.Rect | |
| 25 | + | if x >= r.X && x < r.X+r.W && y >= r.Y && y < r.Y+r.H { | |
| 26 | + | got = "gray" | |
| 27 | + | if s.Active { | |
| 28 | + | got = "lit" | |
| 29 | + | } | |
| 30 | + | } | |
| 31 | + | } | |
| 32 | + | return got | |
| 33 | + | } | |
| 34 | + | ||
| 35 | + | func TestSeparators(t *testing.T) { | |
| 36 | + | type px struct { | |
| 37 | + | x, y int | |
| 38 | + | want string | |
| 39 | + | } | |
| 40 | + | cols2 := []Rect{{X: 0, Y: 0, W: 16, H: 16}, {X: 16, Y: 0, W: 16, H: 16}} | |
| 41 | + | cols3 := []Rect{{X: 0, Y: 0, W: 16, H: 16}, {X: 16, Y: 0, W: 16, H: 16}, {X: 32, Y: 0, W: 16, H: 16}} | |
| 42 | + | // A full-height column and a column split in two. | |
| 43 | + | grid := []Rect{{X: 0, Y: 0, W: 16, H: 32}, {X: 16, Y: 0, W: 16, H: 16}, {X: 16, Y: 16, W: 16, H: 16}} | |
| 44 | + | for _, tc := range []struct { | |
| 45 | + | name string | |
| 46 | + | ps []Placement | |
| 47 | + | lit bool | |
| 48 | + | check []px | |
| 49 | + | }{ | |
| 50 | + | // The left window owns the line (x 14-15); the client of the right | |
| 51 | + | // window is not covered. | |
| 52 | + | {"two columns, left focused: top half", tiles(0, cols2...), true, []px{{14, 3, "lit"}, {15, 7, "lit"}, {14, 8, "gray"}, {14, 15, "gray"}, {16, 3, ""}, {13, 3, ""}}}, | |
| 53 | + | {"two columns, right focused: bottom half", tiles(1, cols2...), true, []px{{14, 3, "gray"}, {14, 12, "lit"}}}, | |
| 54 | + | {"unfocused output: all gray", tiles(0, cols2...), false, []px{{14, 3, "gray"}, {14, 12, "gray"}}}, | |
| 55 | + | {"three columns, middle: both lines whole", tiles(1, cols3...), true, []px{{14, 3, "lit"}, {14, 12, "lit"}, {30, 3, "lit"}, {30, 12, "lit"}}}, | |
| 56 | + | {"three columns, first: its line only", tiles(0, cols3...), true, []px{{14, 3, "lit"}, {14, 12, "lit"}, {30, 3, "gray"}, {30, 12, "gray"}}}, | |
| 57 | + | {"no line at the screen edge", tiles(2, cols3...), true, []px{{47, 3, ""}, {0, 3, ""}}}, | |
| 58 | + | // The left and top lines meet: the corner square is lit. | |
| 59 | + | {"bottom-right tile: corner closed", tiles(2, grid...), true, []px{{14, 30, "lit"}, {14, 20, "lit"}, {14, 14, "lit"}, {14, 15, "lit"}, {14, 5, "gray"}, {20, 14, "lit"}, {31, 15, "lit"}}}, | |
| 60 | + | {"top-right tile", tiles(1, grid...), true, []px{{14, 3, "lit"}, {14, 15, "lit"}, {14, 20, "gray"}, {20, 14, "lit"}}}, | |
| 61 | + | {"left column: whole line, split column untouched", tiles(0, grid...), true, []px{{14, 3, "lit"}, {14, 30, "lit"}, {20, 14, "gray"}}}, | |
| 62 | + | } { | |
| 63 | + | t.Run(tc.name, func(t *testing.T) { | |
| 64 | + | seps := separators(tc.ps, 2, tc.lit) | |
| 65 | + | for _, c := range tc.check { | |
| 66 | + | if got := colorAt(seps, c.x, c.y); got != c.want { | |
| 67 | + | t.Errorf("(%d,%d) = %q, want %q; %v", c.x, c.y, got, c.want, seps) | |
| 68 | + | } | |
| 69 | + | } | |
| 70 | + | }) | |
| 71 | + | } | |
| 72 | + | } | |
| 73 | + | ||
| 74 | + | func TestSeparatorsSkip(t *testing.T) { | |
| 75 | + | if s := separators(tiles(0, Rect{W: 32, H: 16}), 2, true); len(s) != 0 { | |
| 76 | + | t.Fatal("lone window:", s) | |
| 77 | + | } | |
| 78 | + | if s := separators(tiles(0, Rect{W: 16, H: 16}, Rect{X: 16, W: 16, H: 16}), 0, true); len(s) != 0 { | |
| 79 | + | t.Fatal("width 0:", s) | |
| 80 | + | } | |
| 81 | + | full := tiles(0, Rect{W: 16, H: 16}, Rect{X: 16, W: 16, H: 16}) | |
| 82 | + | full[0].Fullscreen, full[1].Hidden = true, true | |
| 83 | + | if s := separators(full, 2, true); len(s) != 0 { | |
| 84 | + | t.Fatal("fullscreen:", s) | |
| 85 | + | } | |
| 86 | + | } | |
| 87 | + | ||
| 88 | + | func TestSeparatorsFloat(t *testing.T) { | |
| 89 | + | float := Placement{ID: 1, Rect: Rect{X: 10, Y: 10, W: 20, H: 20}, Floating: true, Focused: true, Inset: ports.SideAll} | |
| 90 | + | seps := separators([]Placement{float}, 2, true) | |
| 91 | + | for _, c := range [][2]int{{10, 10}, {29, 29}, {20, 11}} { | |
| 92 | + | if got := colorAt(seps, c[0], c[1]); got != "lit" { | |
| 93 | + | t.Errorf("(%d,%d) = %q", c[0], c[1], got) | |
| 94 | + | } | |
| 95 | + | } | |
| 96 | + | if got := colorAt(seps, 20, 20); got != "" { | |
| 97 | + | t.Error("inside:", got) | |
| 98 | + | } | |
| 99 | + | } | |
| @@ -0,0 +1,99 @@ | |||||
| 1 | + | package core | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "testing" | |||
| 5 | + | ||||
| 6 | + | "github.com/bnema/nefertty/internal/ports" | |||
| 7 | + | ) | |||
| 8 | + | ||||
| 9 | + | // tiles lays out rects edge to edge (no gap) with focus on index f. | |||
| 10 | + | func tiles(f int, rs ...Rect) []Placement { | |||
| 11 | + | ps := make([]Placement, len(rs)) | |||
| 12 | + | for i, r := range rs { | |||
| 13 | + | ps[i] = Placement{ID: WindowID(i + 1), Rect: r, Focused: i == f} | |||
| 14 | + | } | |||
| 15 | + | setNeighbors(ps, 0, Rect{W: 1000, H: 1000}) | |||
| 16 | + | return ps | |||
| 17 | + | } | |||
| 18 | + | ||||
| 19 | + | // colorAt is what the renderer shows at (x, y): the last separator over | |||
| 20 | + | // it wins. "" is no line. | |||
| 21 | + | func colorAt(seps []ports.Separator, x, y int) string { | |||
| 22 | + | got := "" | |||
| 23 | + | for _, s := range seps { | |||
| 24 | + | r := s.Rect | |||
| 25 | + | if x >= r.X && x < r.X+r.W && y >= r.Y && y < r.Y+r.H { | |||
| 26 | + | got = "gray" | |||
| 27 | + | if s.Active { | |||
| 28 | + | got = "lit" | |||
| 29 | + | } | |||
| 30 | + | } | |||
| 31 | + | } | |||
| 32 | + | return got | |||
| 33 | + | } | |||
| 34 | + | ||||
| 35 | + | func TestSeparators(t *testing.T) { | |||
| 36 | + | type px struct { | |||
| 37 | + | x, y int | |||
| 38 | + | want string | |||
| 39 | + | } | |||
| 40 | + | cols2 := []Rect{{X: 0, Y: 0, W: 16, H: 16}, {X: 16, Y: 0, W: 16, H: 16}} | |||
| 41 | + | cols3 := []Rect{{X: 0, Y: 0, W: 16, H: 16}, {X: 16, Y: 0, W: 16, H: 16}, {X: 32, Y: 0, W: 16, H: 16}} | |||
| 42 | + | // A full-height column and a column split in two. | |||
| 43 | + | grid := []Rect{{X: 0, Y: 0, W: 16, H: 32}, {X: 16, Y: 0, W: 16, H: 16}, {X: 16, Y: 16, W: 16, H: 16}} | |||
| 44 | + | for _, tc := range []struct { | |||
| 45 | + | name string | |||
| 46 | + | ps []Placement | |||
| 47 | + | lit bool | |||
| 48 | + | check []px | |||
| 49 | + | }{ | |||
| 50 | + | // The left window owns the line (x 14-15); the client of the right | |||
| 51 | + | // window is not covered. | |||
| 52 | + | {"two columns, left focused: top half", tiles(0, cols2...), true, []px{{14, 3, "lit"}, {15, 7, "lit"}, {14, 8, "gray"}, {14, 15, "gray"}, {16, 3, ""}, {13, 3, ""}}}, | |||
| 53 | + | {"two columns, right focused: bottom half", tiles(1, cols2...), true, []px{{14, 3, "gray"}, {14, 12, "lit"}}}, | |||
| 54 | + | {"unfocused output: all gray", tiles(0, cols2...), false, []px{{14, 3, "gray"}, {14, 12, "gray"}}}, | |||
| 55 | + | {"three columns, middle: both lines whole", tiles(1, cols3...), true, []px{{14, 3, "lit"}, {14, 12, "lit"}, {30, 3, "lit"}, {30, 12, "lit"}}}, | |||
| 56 | + | {"three columns, first: its line only", tiles(0, cols3...), true, []px{{14, 3, "lit"}, {14, 12, "lit"}, {30, 3, "gray"}, {30, 12, "gray"}}}, | |||
| 57 | + | {"no line at the screen edge", tiles(2, cols3...), true, []px{{47, 3, ""}, {0, 3, ""}}}, | |||
| 58 | + | // The left and top lines meet: the corner square is lit. | |||
| 59 | + | {"bottom-right tile: corner closed", tiles(2, grid...), true, []px{{14, 30, "lit"}, {14, 20, "lit"}, {14, 14, "lit"}, {14, 15, "lit"}, {14, 5, "gray"}, {20, 14, "lit"}, {31, 15, "lit"}}}, | |||
| 60 | + | {"top-right tile", tiles(1, grid...), true, []px{{14, 3, "lit"}, {14, 15, "lit"}, {14, 20, "gray"}, {20, 14, "lit"}}}, | |||
| 61 | + | {"left column: whole line, split column untouched", tiles(0, grid...), true, []px{{14, 3, "lit"}, {14, 30, "lit"}, {20, 14, "gray"}}}, | |||
| 62 | + | } { | |||
| 63 | + | t.Run(tc.name, func(t *testing.T) { | |||
| 64 | + | seps := separators(tc.ps, 2, tc.lit) | |||
| 65 | + | for _, c := range tc.check { | |||
| 66 | + | if got := colorAt(seps, c.x, c.y); got != c.want { | |||
| 67 | + | t.Errorf("(%d,%d) = %q, want %q; %v", c.x, c.y, got, c.want, seps) | |||
| 68 | + | } | |||
| 69 | + | } | |||
| 70 | + | }) | |||
| 71 | + | } | |||
| 72 | + | } | |||
| 73 | + | ||||
| 74 | + | func TestSeparatorsSkip(t *testing.T) { | |||
| 75 | + | if s := separators(tiles(0, Rect{W: 32, H: 16}), 2, true); len(s) != 0 { | |||
| 76 | + | t.Fatal("lone window:", s) | |||
| 77 | + | } | |||
| 78 | + | if s := separators(tiles(0, Rect{W: 16, H: 16}, Rect{X: 16, W: 16, H: 16}), 0, true); len(s) != 0 { | |||
| 79 | + | t.Fatal("width 0:", s) | |||
| 80 | + | } | |||
| 81 | + | full := tiles(0, Rect{W: 16, H: 16}, Rect{X: 16, W: 16, H: 16}) | |||
| 82 | + | full[0].Fullscreen, full[1].Hidden = true, true | |||
| 83 | + | if s := separators(full, 2, true); len(s) != 0 { | |||
| 84 | + | t.Fatal("fullscreen:", s) | |||
| 85 | + | } | |||
| 86 | + | } | |||
| 87 | + | ||||
| 88 | + | func TestSeparatorsFloat(t *testing.T) { | |||
| 89 | + | float := Placement{ID: 1, Rect: Rect{X: 10, Y: 10, W: 20, H: 20}, Floating: true, Focused: true, Inset: ports.SideAll} | |||
| 90 | + | seps := separators([]Placement{float}, 2, true) | |||
| 91 | + | for _, c := range [][2]int{{10, 10}, {29, 29}, {20, 11}} { | |||
| 92 | + | if got := colorAt(seps, c[0], c[1]); got != "lit" { | |||
| 93 | + | t.Errorf("(%d,%d) = %q", c[0], c[1], got) | |||
| 94 | + | } | |||
| 95 | + | } | |||
| 96 | + | if got := colorAt(seps, 20, 20); got != "" { | |||
| 97 | + | t.Error("inside:", got) | |||
| 98 | + | } | |||
| 99 | + | } | |||
M internal/ports/messages.go +13 -4
| @@ -591,6 +591,9 @@ type Scene struct { | |||
| 591 | 591 | Background string | |
| 592 | 592 | Border Border | |
| 593 | 593 | Windows []SceneWindow | |
| 594 | + | // Separators are the lines between windows, drawn after them in slice | |
| 595 | + | // order with the Border colors. | |
| 596 | + | Separators []Separator | |
| 594 | 597 | // Layers are drawn in slice order: background and bottom before windows, | |
| 595 | 598 | // top and overlay after. A fullscreen window covers bottom and top. | |
| 596 | 599 | // Window popups are drawn after the windows, layer popups | |
| @@ -621,6 +624,13 @@ type Border struct { | |||
| 621 | 624 | Active, Inactive string | |
| 622 | 625 | } | |
| 623 | 626 | ||
| 627 | + | // Separator is a line between windows, in logical pixels. Active lines | |
| 628 | + | // mark the focused window and use Border.Active, others Border.Inactive. | |
| 629 | + | type Separator struct { | |
| 630 | + | Rect Rect | |
| 631 | + | Active bool | |
| 632 | + | } | |
| 633 | + | ||
| 624 | 634 | // SceneLayer carries core → renderer layer surface placement. | |
| 625 | 635 | type SceneLayer struct { | |
| 626 | 636 | ID WindowID | |
| @@ -761,10 +771,9 @@ type SceneWindow struct { | |||
| 761 | 771 | ID WindowID | |
| 762 | 772 | Rect Rect | |
| 763 | 773 | Focused, Fullscreen, Hidden bool | |
| 764 | - | // Inset sides carry the border inside Rect. Neighbors are the sides | |
| 765 | - | // touching another tile: the focused window also lights its neighbors' | |
| 766 | - | // border line on the sides it does not inset. | |
| 767 | - | Neighbors, Inset Sides | |
| 774 | + | // Inset sides carry a separator line inside Rect: the client is drawn | |
| 775 | + | // inside it. | |
| 776 | + | Inset Sides | |
| 768 | 777 | // Popups are drawn from their content only: no border, no background. | |
| 769 | 778 | Popup bool | |
| 770 | 779 | // OverLayers popups hang from a layer surface: drawn over the top and | |
| @@ -591,6 +591,9 @@ type Scene struct { | |||||
| 591 | Background string | 591 | Background string | ||
| 592 | Border Border | 592 | Border Border | ||
| 593 | Windows []SceneWindow | 593 | Windows []SceneWindow | ||
| 594 | + | // Separators are the lines between windows, drawn after them in slice | |||
| 595 | + | // order with the Border colors. | |||
| 596 | + | Separators []Separator | |||
| 594 | // Layers are drawn in slice order: background and bottom before windows, | 597 | // Layers are drawn in slice order: background and bottom before windows, | ||
| 595 | // top and overlay after. A fullscreen window covers bottom and top. | 598 | // top and overlay after. A fullscreen window covers bottom and top. | ||
| 596 | // Window popups are drawn after the windows, layer popups | 599 | // Window popups are drawn after the windows, layer popups | ||
| @@ -621,6 +624,13 @@ type Border struct { | |||||
| 621 | Active, Inactive string | 624 | Active, Inactive string | ||
| 622 | } | 625 | } | ||
| 623 | 626 | ||||
| 627 | + | // Separator is a line between windows, in logical pixels. Active lines | |||
| 628 | + | // mark the focused window and use Border.Active, others Border.Inactive. | |||
| 629 | + | type Separator struct { | |||
| 630 | + | Rect Rect | |||
| 631 | + | Active bool | |||
| 632 | + | } | |||
| 633 | + | ||||
| 624 | // SceneLayer carries core → renderer layer surface placement. | 634 | // SceneLayer carries core → renderer layer surface placement. | ||
| 625 | type SceneLayer struct { | 635 | type SceneLayer struct { | ||
| 626 | ID WindowID | 636 | ID WindowID | ||
| @@ -761,10 +771,9 @@ type SceneWindow struct { | |||||
| 761 | ID WindowID | 771 | ID WindowID | ||
| 762 | Rect Rect | 772 | Rect Rect | ||
| 763 | Focused, Fullscreen, Hidden bool | 773 | Focused, Fullscreen, Hidden bool | ||
| 764 | - | // Inset sides carry the border inside Rect. Neighbors are the sides | 774 | + | // Inset sides carry a separator line inside Rect: the client is drawn |
| 765 | - | // touching another tile: the focused window also lights its neighbors' | 775 | + | // inside it. |
| 766 | - | // border line on the sides it does not inset. | 776 | + | Inset Sides |
| 767 | - | Neighbors, Inset Sides | |||
| 768 | // Popups are drawn from their content only: no border, no background. | 777 | // Popups are drawn from their content only: no border, no background. | ||
| 769 | Popup bool | 778 | Popup bool | ||
| 770 | // OverLayers popups hang from a layer surface: drawn over the top and | 779 | // OverLayers popups hang from a layer surface: drawn over the top and | ||