refactor(core): build each screen's scene in sceneFor, out of publishFrame
- commit
- 794cca943ac1e828e601f5fc336a361291136f15
- author
- brice · · 2026-10-08 05:53 UTC
- parent
- 208528f
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +146 -127
M internal/core/core.go +4 -127
| @@ -587,134 +587,11 @@ func (c *Core) publishFrame(ctx context.Context, only *screen) error { | |||
| 587 | 587 | // Before the first output (and after the last is unplugged) the | |
| 588 | 588 | // placeholder's scene has no output name: no renderer draws it. | |
| 589 | 589 | for i, sc := range c.screens { | |
| 590 | - | o := sc.mon.Output() | |
| 591 | - | // frame is the viewport of the workspace on screen: the whole output | |
| 592 | - | // unless it has a size override (never in the overview). | |
| 593 | - | frame := sc.mon.Frame() | |
| 594 | - | var clip Rect | |
| 595 | - | if frame != (Rect{W: o.W, H: o.H}) { | |
| 596 | - | clip = frame | |
| 597 | - | } | |
| 598 | - | // layout is what is drawn; settled (same indexes) is where the | |
| 599 | - | // windows are going, and alone sizes the configures. | |
| 600 | - | layout, settled := sc.shown, sc.settledLayout | |
| 601 | - | scene := ports.Scene{Security: c.security, Output: sc.name(), OutputWidth: o.W, OutputHeight: o.H, WorkspaceClip: clip, Scale: sc.scale, Transform: sc.transform, Off: sc.off, 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, len(layout)+len(c.popupOrder)), Layers: shownLayers(sc)} | |
| 602 | - | var real map[WindowID]Placement | |
| 603 | - | if sc.mon.ov.open { | |
| 604 | - | // Local to this screen's build: reused, cleared each time. | |
| 605 | - | if c.overviewReal == nil { | |
| 606 | - | c.overviewReal = make(map[WindowID]Placement) | |
| 607 | - | } | |
| 608 | - | clear(c.overviewReal) | |
| 609 | - | real = c.overviewReal | |
| 610 | - | for w := range sc.mon.all() { | |
| 611 | - | c.realBuf = w.layoutInto(c.realBuf) | |
| 612 | - | for _, p := range c.realBuf { | |
| 613 | - | real[p.ID] = p | |
| 614 | - | } | |
| 615 | - | } | |
| 616 | - | } | |
| 617 | - | scene.Dim = floatDim(layout, frame, c.cfg.Floating.Dim) | |
| 618 | - | if sc.mon.ov.open { | |
| 619 | - | // Darken the wallpaper around the previews. | |
| 620 | - | scene.Dim = c.cfg.Floating.Dim | |
| 621 | - | scene.DimBehind = scene.Dim > 0 | |
| 622 | - | } | |
| 623 | - | if d := c.drag; d != nil && d.target.screen == sc && d.target.kind != dropNone { | |
| 624 | - | scene.DropHints = slices.Clone(d.target.hints) | |
| 625 | - | } | |
| 626 | - | if sc.mon.ov.open { | |
| 627 | - | // Frame the selection and separate numbered and named row groups. | |
| 628 | - | _, scene.Separators = sc.mon.overviewRows() | |
| 629 | - | scene.Separators = append(scene.Separators, overviewOutline(layout, max(c.cfg.Border.Width, 2))...) | |
| 630 | - | } else { | |
| 631 | - | // Only the focused output lights the focused window's lines. | |
| 632 | - | scene.Separators = separators(layout, c.cfg.Border.Width, sc.mon.Current().gap(), frame, i == c.focusScreen) | |
| 633 | - | } | |
| 634 | - | // A window alone on screen needs no pulse to show it has the focus. | |
| 635 | - | // A tile under a panel (the next cascade band) does not count. | |
| 636 | - | alone := i == c.focusScreen && c.pulse.target != 0 && visibleCount(layout, sc.mon.Current().Usable) == 1 | |
| 637 | - | for k, p := range layout { | |
| 638 | - | ps := settled[k] | |
| 639 | - | // Only the focused output has an activated window. | |
| 640 | - | focused := p.Focused && i == c.focusScreen | |
| 641 | - | sw := ports.SceneWindow{ID: p.ID, Rect: p.Rect, Focused: focused, Fullscreen: p.Fullscreen, Hidden: p.Hidden, Floating: p.Floating, Below: p.Below, Inset: p.Inset, Preview: p.Preview, Fade: p.Fade} | |
| 642 | - | if p.Zoom > 0 && (p.Zoom < 1 || p.Preview > 0) { | |
| 643 | - | // A scale motion: the content follows the drawn size. A | |
| 644 | - | // card in flight drawn at its size still needs Zoom 1: | |
| 645 | - | // without it the renderer would shrink it by Preview. | |
| 646 | - | sw.Zoom = p.Zoom | |
| 647 | - | } | |
| 648 | - | // A peek's veil is the configured one plus its animated offset. | |
| 649 | - | sw.Dim = max(0, min(p.Dim+c.peekDim(p), 1)) | |
| 650 | - | if p.Leaving { | |
| 651 | - | // A window fading out after it closed or hid: drawn, but | |
| 652 | - | // hidden to everything else, its configures included. | |
| 653 | - | sw.Hidden = false | |
| 654 | - | scene.Windows = append(scene.Windows, sw) | |
| 655 | - | // A window the workspace still holds (a hidden stash | |
| 656 | - | // window) keeps its last configure: prune would forget it, | |
| 657 | - | // and its next one would start from nothing. A closed one | |
| 658 | - | // was forgotten at its unmap and stays so. | |
| 659 | - | if w, _ := sc.mon.find(p.ID); w != nil { | |
| 660 | - | c.configures.keep(p.ID) | |
| 661 | - | } | |
| 662 | - | continue | |
| 663 | - | } | |
| 664 | - | if focused && p.ID == c.pulse.target && !alone && !p.Fullscreen && !p.Hidden && p.Preview == 0 && !sc.mon.ov.open { | |
| 665 | - | drawable = true | |
| 666 | - | if p.ID == c.pulse.id { | |
| 667 | - | sw.FocusEffect = pulse | |
| 668 | - | } | |
| 669 | - | } | |
| 670 | - | scene.Windows = append(scene.Windows, sw) | |
| 671 | - | t := configureTarget{output: sc.name(), area: frame, focused: focused, captured: capture != nil && capture.window == p.ID} | |
| 672 | - | if !p.Hidden && p.Preview == 0 { | |
| 673 | - | // Only a sized configure needs the client size. | |
| 674 | - | t.client, t.imposed = c.clientRect(ps), sc.mon.Current().imposedFloat(p.ID) | |
| 675 | - | } else if t.captured && p.Hidden { | |
| 676 | - | // A captured hidden window is sized like its capture. | |
| 677 | - | t.client = capture.windowSz | |
| 678 | - | } else if p.Preview > 0 && !p.Hidden { | |
| 679 | - | if rp, ok := real[p.ID]; ok && !rp.Hidden { | |
| 680 | - | t.realTiled = !rp.Floating | |
| 681 | - | t.client = c.clientRect(rp) | |
| 682 | - | } | |
| 683 | - | } | |
| 684 | - | cp, t := c.captureConfigure(sc, ps, t) | |
| 685 | - | if v, send := c.configures.nextWithCapture(ps, t, cp); send { | |
| 686 | - | if err := c.command(ctx, v); err != nil { | |
| 687 | - | return err | |
| 688 | - | } | |
| 689 | - | c.configures.mark(v) | |
| 690 | - | } | |
| 691 | - | } | |
| 692 | - | scene.Windows = append(scene.Windows, c.scenePopups(sc)...) | |
| 693 | - | scene.CaptureIndicators = c.captureIndicators(sc) | |
| 694 | - | scene.Capture = c.captureSceneFor(sc, capture) | |
| 695 | - | // A scene carrying a capture image always gets a fresh Seq, and so | |
| 696 | - | // does one of an animating screen (the one being stepped, or any | |
| 697 | - | // when none is): the outputs drop a scene they already show, and a | |
| 698 | - | // spring that starts at the previous scene, or rounds to it in its | |
| 699 | - | // tail, would get no flip and wait for the fallback timer. Any other | |
| 700 | - | // keeps its output's Seq while it draws the same, so an idle output | |
| 701 | - | // is not recomposed. | |
| 702 | - | withCapture := scene.Capture != nil && (sc == capture.hiddenScr || sc == capture.windowScr) | |
| 703 | - | animating := (sc.springing() || c.pulsing(sc)) && (only == nil || only == sc) | |
| 704 | - | if !withCapture && !animating && sc.last.Seq != 0 && scene.SameAs(sc.last) { | |
| 705 | - | scene.Seq = sc.last.Seq | |
| 706 | - | } else { | |
| 707 | - | c.seq++ | |
| 708 | - | scene.Seq = c.seq | |
| 709 | - | } | |
| 710 | - | if withCapture { | |
| 711 | - | switch sc { | |
| 712 | - | case capture.hiddenScr: | |
| 713 | - | scene.CaptureScene = c.captureScene(scene.Seq) | |
| 714 | - | case capture.windowScr: | |
| 715 | - | scene.CaptureScene = c.captureWindowScene(capture, scene.Seq) | |
| 716 | - | } | |
| 590 | + | scene, shown, err := c.sceneFor(ctx, i, sc, only, capture, pulse) | |
| 591 | + | if err != nil { | |
| 592 | + | return err | |
| 717 | 593 | } | |
| 594 | + | drawable = drawable || shown | |
| 718 | 595 | sc.last = scene | |
| 719 | 596 | scenes = append(scenes, scene) | |
| 720 | 597 | } | |
| @@ -587,134 +587,11 @@ func (c *Core) publishFrame(ctx context.Context, only *screen) error { | |||||
| 587 | // Before the first output (and after the last is unplugged) the | 587 | // Before the first output (and after the last is unplugged) the | ||
| 588 | // placeholder's scene has no output name: no renderer draws it. | 588 | // placeholder's scene has no output name: no renderer draws it. | ||
| 589 | for i, sc := range c.screens { | 589 | for i, sc := range c.screens { | ||
| 590 | - | o := sc.mon.Output() | 590 | + | scene, shown, err := c.sceneFor(ctx, i, sc, only, capture, pulse) |
| 591 | - | // frame is the viewport of the workspace on screen: the whole output | 591 | + | if err != nil { |
| 592 | - | // unless it has a size override (never in the overview). | 592 | + | return err |
| 593 | - | frame := sc.mon.Frame() | |||
| 594 | - | var clip Rect | |||
| 595 | - | if frame != (Rect{W: o.W, H: o.H}) { | |||
| 596 | - | clip = frame | |||
| 597 | - | } | |||
| 598 | - | // layout is what is drawn; settled (same indexes) is where the | |||
| 599 | - | // windows are going, and alone sizes the configures. | |||
| 600 | - | layout, settled := sc.shown, sc.settledLayout | |||
| 601 | - | scene := ports.Scene{Security: c.security, Output: sc.name(), OutputWidth: o.W, OutputHeight: o.H, WorkspaceClip: clip, Scale: sc.scale, Transform: sc.transform, Off: sc.off, 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, len(layout)+len(c.popupOrder)), Layers: shownLayers(sc)} | |||
| 602 | - | var real map[WindowID]Placement | |||
| 603 | - | if sc.mon.ov.open { | |||
| 604 | - | // Local to this screen's build: reused, cleared each time. | |||
| 605 | - | if c.overviewReal == nil { | |||
| 606 | - | c.overviewReal = make(map[WindowID]Placement) | |||
| 607 | - | } | |||
| 608 | - | clear(c.overviewReal) | |||
| 609 | - | real = c.overviewReal | |||
| 610 | - | for w := range sc.mon.all() { | |||
| 611 | - | c.realBuf = w.layoutInto(c.realBuf) | |||
| 612 | - | for _, p := range c.realBuf { | |||
| 613 | - | real[p.ID] = p | |||
| 614 | - | } | |||
| 615 | - | } | |||
| 616 | - | } | |||
| 617 | - | scene.Dim = floatDim(layout, frame, c.cfg.Floating.Dim) | |||
| 618 | - | if sc.mon.ov.open { | |||
| 619 | - | // Darken the wallpaper around the previews. | |||
| 620 | - | scene.Dim = c.cfg.Floating.Dim | |||
| 621 | - | scene.DimBehind = scene.Dim > 0 | |||
| 622 | - | } | |||
| 623 | - | if d := c.drag; d != nil && d.target.screen == sc && d.target.kind != dropNone { | |||
| 624 | - | scene.DropHints = slices.Clone(d.target.hints) | |||
| 625 | - | } | |||
| 626 | - | if sc.mon.ov.open { | |||
| 627 | - | // Frame the selection and separate numbered and named row groups. | |||
| 628 | - | _, scene.Separators = sc.mon.overviewRows() | |||
| 629 | - | scene.Separators = append(scene.Separators, overviewOutline(layout, max(c.cfg.Border.Width, 2))...) | |||
| 630 | - | } else { | |||
| 631 | - | // Only the focused output lights the focused window's lines. | |||
| 632 | - | scene.Separators = separators(layout, c.cfg.Border.Width, sc.mon.Current().gap(), frame, i == c.focusScreen) | |||
| 633 | - | } | |||
| 634 | - | // A window alone on screen needs no pulse to show it has the focus. | |||
| 635 | - | // A tile under a panel (the next cascade band) does not count. | |||
| 636 | - | alone := i == c.focusScreen && c.pulse.target != 0 && visibleCount(layout, sc.mon.Current().Usable) == 1 | |||
| 637 | - | for k, p := range layout { | |||
| 638 | - | ps := settled[k] | |||
| 639 | - | // Only the focused output has an activated window. | |||
| 640 | - | focused := p.Focused && i == c.focusScreen | |||
| 641 | - | sw := ports.SceneWindow{ID: p.ID, Rect: p.Rect, Focused: focused, Fullscreen: p.Fullscreen, Hidden: p.Hidden, Floating: p.Floating, Below: p.Below, Inset: p.Inset, Preview: p.Preview, Fade: p.Fade} | |||
| 642 | - | if p.Zoom > 0 && (p.Zoom < 1 || p.Preview > 0) { | |||
| 643 | - | // A scale motion: the content follows the drawn size. A | |||
| 644 | - | // card in flight drawn at its size still needs Zoom 1: | |||
| 645 | - | // without it the renderer would shrink it by Preview. | |||
| 646 | - | sw.Zoom = p.Zoom | |||
| 647 | - | } | |||
| 648 | - | // A peek's veil is the configured one plus its animated offset. | |||
| 649 | - | sw.Dim = max(0, min(p.Dim+c.peekDim(p), 1)) | |||
| 650 | - | if p.Leaving { | |||
| 651 | - | // A window fading out after it closed or hid: drawn, but | |||
| 652 | - | // hidden to everything else, its configures included. | |||
| 653 | - | sw.Hidden = false | |||
| 654 | - | scene.Windows = append(scene.Windows, sw) | |||
| 655 | - | // A window the workspace still holds (a hidden stash | |||
| 656 | - | // window) keeps its last configure: prune would forget it, | |||
| 657 | - | // and its next one would start from nothing. A closed one | |||
| 658 | - | // was forgotten at its unmap and stays so. | |||
| 659 | - | if w, _ := sc.mon.find(p.ID); w != nil { | |||
| 660 | - | c.configures.keep(p.ID) | |||
| 661 | - | } | |||
| 662 | - | continue | |||
| 663 | - | } | |||
| 664 | - | if focused && p.ID == c.pulse.target && !alone && !p.Fullscreen && !p.Hidden && p.Preview == 0 && !sc.mon.ov.open { | |||
| 665 | - | drawable = true | |||
| 666 | - | if p.ID == c.pulse.id { | |||
| 667 | - | sw.FocusEffect = pulse | |||
| 668 | - | } | |||
| 669 | - | } | |||
| 670 | - | scene.Windows = append(scene.Windows, sw) | |||
| 671 | - | t := configureTarget{output: sc.name(), area: frame, focused: focused, captured: capture != nil && capture.window == p.ID} | |||
| 672 | - | if !p.Hidden && p.Preview == 0 { | |||
| 673 | - | // Only a sized configure needs the client size. | |||
| 674 | - | t.client, t.imposed = c.clientRect(ps), sc.mon.Current().imposedFloat(p.ID) | |||
| 675 | - | } else if t.captured && p.Hidden { | |||
| 676 | - | // A captured hidden window is sized like its capture. | |||
| 677 | - | t.client = capture.windowSz | |||
| 678 | - | } else if p.Preview > 0 && !p.Hidden { | |||
| 679 | - | if rp, ok := real[p.ID]; ok && !rp.Hidden { | |||
| 680 | - | t.realTiled = !rp.Floating | |||
| 681 | - | t.client = c.clientRect(rp) | |||
| 682 | - | } | |||
| 683 | - | } | |||
| 684 | - | cp, t := c.captureConfigure(sc, ps, t) | |||
| 685 | - | if v, send := c.configures.nextWithCapture(ps, t, cp); send { | |||
| 686 | - | if err := c.command(ctx, v); err != nil { | |||
| 687 | - | return err | |||
| 688 | - | } | |||
| 689 | - | c.configures.mark(v) | |||
| 690 | - | } | |||
| 691 | - | } | |||
| 692 | - | scene.Windows = append(scene.Windows, c.scenePopups(sc)...) | |||
| 693 | - | scene.CaptureIndicators = c.captureIndicators(sc) | |||
| 694 | - | scene.Capture = c.captureSceneFor(sc, capture) | |||
| 695 | - | // A scene carrying a capture image always gets a fresh Seq, and so | |||
| 696 | - | // does one of an animating screen (the one being stepped, or any | |||
| 697 | - | // when none is): the outputs drop a scene they already show, and a | |||
| 698 | - | // spring that starts at the previous scene, or rounds to it in its | |||
| 699 | - | // tail, would get no flip and wait for the fallback timer. Any other | |||
| 700 | - | // keeps its output's Seq while it draws the same, so an idle output | |||
| 701 | - | // is not recomposed. | |||
| 702 | - | withCapture := scene.Capture != nil && (sc == capture.hiddenScr || sc == capture.windowScr) | |||
| 703 | - | animating := (sc.springing() || c.pulsing(sc)) && (only == nil || only == sc) | |||
| 704 | - | if !withCapture && !animating && sc.last.Seq != 0 && scene.SameAs(sc.last) { | |||
| 705 | - | scene.Seq = sc.last.Seq | |||
| 706 | - | } else { | |||
| 707 | - | c.seq++ | |||
| 708 | - | scene.Seq = c.seq | |||
| 709 | - | } | |||
| 710 | - | if withCapture { | |||
| 711 | - | switch sc { | |||
| 712 | - | case capture.hiddenScr: | |||
| 713 | - | scene.CaptureScene = c.captureScene(scene.Seq) | |||
| 714 | - | case capture.windowScr: | |||
| 715 | - | scene.CaptureScene = c.captureWindowScene(capture, scene.Seq) | |||
| 716 | - | } | |||
| 717 | } | 593 | } | ||
| 594 | + | drawable = drawable || shown | |||
| 718 | sc.last = scene | 595 | sc.last = scene | ||
| 719 | scenes = append(scenes, scene) | 596 | scenes = append(scenes, scene) | ||
| 720 | } | 597 | } | ||
A internal/core/scene.go +142 -0
| @@ -0,0 +1,142 @@ | |||
| 1 | + | package core | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "slices" | |
| 6 | + | ||
| 7 | + | "github.com/bnema/neferwl/internal/ports" | |
| 8 | + | ) | |
| 9 | + | ||
| 10 | + | // sceneFor builds the scene of screen i and sends the configures it sizes. | |
| 11 | + | // drawable reports that it can show the focus pulse on the focused window. | |
| 12 | + | func (c *Core) sceneFor(ctx context.Context, i int, sc *screen, only *screen, capture *capView, pulse float64) (scene ports.Scene, drawable bool, err error) { | |
| 13 | + | o := sc.mon.Output() | |
| 14 | + | // frame is the viewport of the workspace on screen: the whole output | |
| 15 | + | // unless it has a size override (never in the overview). | |
| 16 | + | frame := sc.mon.Frame() | |
| 17 | + | var clip Rect | |
| 18 | + | if frame != (Rect{W: o.W, H: o.H}) { | |
| 19 | + | clip = frame | |
| 20 | + | } | |
| 21 | + | // layout is what is drawn; settled (same indexes) is where the | |
| 22 | + | // windows are going, and alone sizes the configures. | |
| 23 | + | layout, settled := sc.shown, sc.settledLayout | |
| 24 | + | scene = ports.Scene{Security: c.security, Output: sc.name(), OutputWidth: o.W, OutputHeight: o.H, WorkspaceClip: clip, Scale: sc.scale, Transform: sc.transform, Off: sc.off, 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, len(layout)+len(c.popupOrder)), Layers: shownLayers(sc)} | |
| 25 | + | var real map[WindowID]Placement | |
| 26 | + | if sc.mon.ov.open { | |
| 27 | + | // Local to this screen's build: reused, cleared each time. | |
| 28 | + | if c.overviewReal == nil { | |
| 29 | + | c.overviewReal = make(map[WindowID]Placement) | |
| 30 | + | } | |
| 31 | + | clear(c.overviewReal) | |
| 32 | + | real = c.overviewReal | |
| 33 | + | for w := range sc.mon.all() { | |
| 34 | + | c.realBuf = w.layoutInto(c.realBuf) | |
| 35 | + | for _, p := range c.realBuf { | |
| 36 | + | real[p.ID] = p | |
| 37 | + | } | |
| 38 | + | } | |
| 39 | + | } | |
| 40 | + | scene.Dim = floatDim(layout, frame, c.cfg.Floating.Dim) | |
| 41 | + | if sc.mon.ov.open { | |
| 42 | + | // Darken the wallpaper around the previews. | |
| 43 | + | scene.Dim = c.cfg.Floating.Dim | |
| 44 | + | scene.DimBehind = scene.Dim > 0 | |
| 45 | + | } | |
| 46 | + | if d := c.drag; d != nil && d.target.screen == sc && d.target.kind != dropNone { | |
| 47 | + | scene.DropHints = slices.Clone(d.target.hints) | |
| 48 | + | } | |
| 49 | + | if sc.mon.ov.open { | |
| 50 | + | // Frame the selection and separate numbered and named row groups. | |
| 51 | + | _, scene.Separators = sc.mon.overviewRows() | |
| 52 | + | scene.Separators = append(scene.Separators, overviewOutline(layout, max(c.cfg.Border.Width, 2))...) | |
| 53 | + | } else { | |
| 54 | + | // Only the focused output lights the focused window's lines. | |
| 55 | + | scene.Separators = separators(layout, c.cfg.Border.Width, sc.mon.Current().gap(), frame, i == c.focusScreen) | |
| 56 | + | } | |
| 57 | + | // A window alone on screen needs no pulse to show it has the focus. | |
| 58 | + | // A tile under a panel (the next cascade band) does not count. | |
| 59 | + | alone := i == c.focusScreen && c.pulse.target != 0 && visibleCount(layout, sc.mon.Current().Usable) == 1 | |
| 60 | + | for k, p := range layout { | |
| 61 | + | ps := settled[k] | |
| 62 | + | // Only the focused output has an activated window. | |
| 63 | + | focused := p.Focused && i == c.focusScreen | |
| 64 | + | sw := ports.SceneWindow{ID: p.ID, Rect: p.Rect, Focused: focused, Fullscreen: p.Fullscreen, Hidden: p.Hidden, Floating: p.Floating, Below: p.Below, Inset: p.Inset, Preview: p.Preview, Fade: p.Fade} | |
| 65 | + | if p.Zoom > 0 && (p.Zoom < 1 || p.Preview > 0) { | |
| 66 | + | // A scale motion: the content follows the drawn size. A | |
| 67 | + | // card in flight drawn at its size still needs Zoom 1: | |
| 68 | + | // without it the renderer would shrink it by Preview. | |
| 69 | + | sw.Zoom = p.Zoom | |
| 70 | + | } | |
| 71 | + | // A peek's veil is the configured one plus its animated offset. | |
| 72 | + | sw.Dim = max(0, min(p.Dim+c.peekDim(p), 1)) | |
| 73 | + | if p.Leaving { | |
| 74 | + | // A window fading out after it closed or hid: drawn, but | |
| 75 | + | // hidden to everything else, its configures included. | |
| 76 | + | sw.Hidden = false | |
| 77 | + | scene.Windows = append(scene.Windows, sw) | |
| 78 | + | // A window the workspace still holds (a hidden stash | |
| 79 | + | // window) keeps its last configure: prune would forget it, | |
| 80 | + | // and its next one would start from nothing. A closed one | |
| 81 | + | // was forgotten at its unmap and stays so. | |
| 82 | + | if w, _ := sc.mon.find(p.ID); w != nil { | |
| 83 | + | c.configures.keep(p.ID) | |
| 84 | + | } | |
| 85 | + | continue | |
| 86 | + | } | |
| 87 | + | if focused && p.ID == c.pulse.target && !alone && !p.Fullscreen && !p.Hidden && p.Preview == 0 && !sc.mon.ov.open { | |
| 88 | + | drawable = true | |
| 89 | + | if p.ID == c.pulse.id { | |
| 90 | + | sw.FocusEffect = pulse | |
| 91 | + | } | |
| 92 | + | } | |
| 93 | + | scene.Windows = append(scene.Windows, sw) | |
| 94 | + | t := configureTarget{output: sc.name(), area: frame, focused: focused, captured: capture != nil && capture.window == p.ID} | |
| 95 | + | if !p.Hidden && p.Preview == 0 { | |
| 96 | + | // Only a sized configure needs the client size. | |
| 97 | + | t.client, t.imposed = c.clientRect(ps), sc.mon.Current().imposedFloat(p.ID) | |
| 98 | + | } else if t.captured && p.Hidden { | |
| 99 | + | // A captured hidden window is sized like its capture. | |
| 100 | + | t.client = capture.windowSz | |
| 101 | + | } else if p.Preview > 0 && !p.Hidden { | |
| 102 | + | if rp, ok := real[p.ID]; ok && !rp.Hidden { | |
| 103 | + | t.realTiled = !rp.Floating | |
| 104 | + | t.client = c.clientRect(rp) | |
| 105 | + | } | |
| 106 | + | } | |
| 107 | + | cp, t := c.captureConfigure(sc, ps, t) | |
| 108 | + | if v, send := c.configures.nextWithCapture(ps, t, cp); send { | |
| 109 | + | if err := c.command(ctx, v); err != nil { | |
| 110 | + | return ports.Scene{}, false, err | |
| 111 | + | } | |
| 112 | + | c.configures.mark(v) | |
| 113 | + | } | |
| 114 | + | } | |
| 115 | + | scene.Windows = append(scene.Windows, c.scenePopups(sc)...) | |
| 116 | + | scene.CaptureIndicators = c.captureIndicators(sc) | |
| 117 | + | scene.Capture = c.captureSceneFor(sc, capture) | |
| 118 | + | // A scene carrying a capture image always gets a fresh Seq, and so | |
| 119 | + | // does one of an animating screen (the one being stepped, or any | |
| 120 | + | // when none is): the outputs drop a scene they already show, and a | |
| 121 | + | // spring that starts at the previous scene, or rounds to it in its | |
| 122 | + | // tail, would get no flip and wait for the fallback timer. Any other | |
| 123 | + | // keeps its output's Seq while it draws the same, so an idle output | |
| 124 | + | // is not recomposed. | |
| 125 | + | withCapture := scene.Capture != nil && (sc == capture.hiddenScr || sc == capture.windowScr) | |
| 126 | + | animating := (sc.springing() || c.pulsing(sc)) && (only == nil || only == sc) | |
| 127 | + | if !withCapture && !animating && sc.last.Seq != 0 && scene.SameAs(sc.last) { | |
| 128 | + | scene.Seq = sc.last.Seq | |
| 129 | + | } else { | |
| 130 | + | c.seq++ | |
| 131 | + | scene.Seq = c.seq | |
| 132 | + | } | |
| 133 | + | if withCapture { | |
| 134 | + | switch sc { | |
| 135 | + | case capture.hiddenScr: | |
| 136 | + | scene.CaptureScene = c.captureScene(scene.Seq) | |
| 137 | + | case capture.windowScr: | |
| 138 | + | scene.CaptureScene = c.captureWindowScene(capture, scene.Seq) | |
| 139 | + | } | |
| 140 | + | } | |
| 141 | + | return scene, drawable, nil | |
| 142 | + | } | |
| @@ -0,0 +1,142 @@ | |||||
| 1 | + | package core | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "slices" | |||
| 6 | + | ||||
| 7 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 8 | + | ) | |||
| 9 | + | ||||
| 10 | + | // sceneFor builds the scene of screen i and sends the configures it sizes. | |||
| 11 | + | // drawable reports that it can show the focus pulse on the focused window. | |||
| 12 | + | func (c *Core) sceneFor(ctx context.Context, i int, sc *screen, only *screen, capture *capView, pulse float64) (scene ports.Scene, drawable bool, err error) { | |||
| 13 | + | o := sc.mon.Output() | |||
| 14 | + | // frame is the viewport of the workspace on screen: the whole output | |||
| 15 | + | // unless it has a size override (never in the overview). | |||
| 16 | + | frame := sc.mon.Frame() | |||
| 17 | + | var clip Rect | |||
| 18 | + | if frame != (Rect{W: o.W, H: o.H}) { | |||
| 19 | + | clip = frame | |||
| 20 | + | } | |||
| 21 | + | // layout is what is drawn; settled (same indexes) is where the | |||
| 22 | + | // windows are going, and alone sizes the configures. | |||
| 23 | + | layout, settled := sc.shown, sc.settledLayout | |||
| 24 | + | scene = ports.Scene{Security: c.security, Output: sc.name(), OutputWidth: o.W, OutputHeight: o.H, WorkspaceClip: clip, Scale: sc.scale, Transform: sc.transform, Off: sc.off, 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, len(layout)+len(c.popupOrder)), Layers: shownLayers(sc)} | |||
| 25 | + | var real map[WindowID]Placement | |||
| 26 | + | if sc.mon.ov.open { | |||
| 27 | + | // Local to this screen's build: reused, cleared each time. | |||
| 28 | + | if c.overviewReal == nil { | |||
| 29 | + | c.overviewReal = make(map[WindowID]Placement) | |||
| 30 | + | } | |||
| 31 | + | clear(c.overviewReal) | |||
| 32 | + | real = c.overviewReal | |||
| 33 | + | for w := range sc.mon.all() { | |||
| 34 | + | c.realBuf = w.layoutInto(c.realBuf) | |||
| 35 | + | for _, p := range c.realBuf { | |||
| 36 | + | real[p.ID] = p | |||
| 37 | + | } | |||
| 38 | + | } | |||
| 39 | + | } | |||
| 40 | + | scene.Dim = floatDim(layout, frame, c.cfg.Floating.Dim) | |||
| 41 | + | if sc.mon.ov.open { | |||
| 42 | + | // Darken the wallpaper around the previews. | |||
| 43 | + | scene.Dim = c.cfg.Floating.Dim | |||
| 44 | + | scene.DimBehind = scene.Dim > 0 | |||
| 45 | + | } | |||
| 46 | + | if d := c.drag; d != nil && d.target.screen == sc && d.target.kind != dropNone { | |||
| 47 | + | scene.DropHints = slices.Clone(d.target.hints) | |||
| 48 | + | } | |||
| 49 | + | if sc.mon.ov.open { | |||
| 50 | + | // Frame the selection and separate numbered and named row groups. | |||
| 51 | + | _, scene.Separators = sc.mon.overviewRows() | |||
| 52 | + | scene.Separators = append(scene.Separators, overviewOutline(layout, max(c.cfg.Border.Width, 2))...) | |||
| 53 | + | } else { | |||
| 54 | + | // Only the focused output lights the focused window's lines. | |||
| 55 | + | scene.Separators = separators(layout, c.cfg.Border.Width, sc.mon.Current().gap(), frame, i == c.focusScreen) | |||
| 56 | + | } | |||
| 57 | + | // A window alone on screen needs no pulse to show it has the focus. | |||
| 58 | + | // A tile under a panel (the next cascade band) does not count. | |||
| 59 | + | alone := i == c.focusScreen && c.pulse.target != 0 && visibleCount(layout, sc.mon.Current().Usable) == 1 | |||
| 60 | + | for k, p := range layout { | |||
| 61 | + | ps := settled[k] | |||
| 62 | + | // Only the focused output has an activated window. | |||
| 63 | + | focused := p.Focused && i == c.focusScreen | |||
| 64 | + | sw := ports.SceneWindow{ID: p.ID, Rect: p.Rect, Focused: focused, Fullscreen: p.Fullscreen, Hidden: p.Hidden, Floating: p.Floating, Below: p.Below, Inset: p.Inset, Preview: p.Preview, Fade: p.Fade} | |||
| 65 | + | if p.Zoom > 0 && (p.Zoom < 1 || p.Preview > 0) { | |||
| 66 | + | // A scale motion: the content follows the drawn size. A | |||
| 67 | + | // card in flight drawn at its size still needs Zoom 1: | |||
| 68 | + | // without it the renderer would shrink it by Preview. | |||
| 69 | + | sw.Zoom = p.Zoom | |||
| 70 | + | } | |||
| 71 | + | // A peek's veil is the configured one plus its animated offset. | |||
| 72 | + | sw.Dim = max(0, min(p.Dim+c.peekDim(p), 1)) | |||
| 73 | + | if p.Leaving { | |||
| 74 | + | // A window fading out after it closed or hid: drawn, but | |||
| 75 | + | // hidden to everything else, its configures included. | |||
| 76 | + | sw.Hidden = false | |||
| 77 | + | scene.Windows = append(scene.Windows, sw) | |||
| 78 | + | // A window the workspace still holds (a hidden stash | |||
| 79 | + | // window) keeps its last configure: prune would forget it, | |||
| 80 | + | // and its next one would start from nothing. A closed one | |||
| 81 | + | // was forgotten at its unmap and stays so. | |||
| 82 | + | if w, _ := sc.mon.find(p.ID); w != nil { | |||
| 83 | + | c.configures.keep(p.ID) | |||
| 84 | + | } | |||
| 85 | + | continue | |||
| 86 | + | } | |||
| 87 | + | if focused && p.ID == c.pulse.target && !alone && !p.Fullscreen && !p.Hidden && p.Preview == 0 && !sc.mon.ov.open { | |||
| 88 | + | drawable = true | |||
| 89 | + | if p.ID == c.pulse.id { | |||
| 90 | + | sw.FocusEffect = pulse | |||
| 91 | + | } | |||
| 92 | + | } | |||
| 93 | + | scene.Windows = append(scene.Windows, sw) | |||
| 94 | + | t := configureTarget{output: sc.name(), area: frame, focused: focused, captured: capture != nil && capture.window == p.ID} | |||
| 95 | + | if !p.Hidden && p.Preview == 0 { | |||
| 96 | + | // Only a sized configure needs the client size. | |||
| 97 | + | t.client, t.imposed = c.clientRect(ps), sc.mon.Current().imposedFloat(p.ID) | |||
| 98 | + | } else if t.captured && p.Hidden { | |||
| 99 | + | // A captured hidden window is sized like its capture. | |||
| 100 | + | t.client = capture.windowSz | |||
| 101 | + | } else if p.Preview > 0 && !p.Hidden { | |||
| 102 | + | if rp, ok := real[p.ID]; ok && !rp.Hidden { | |||
| 103 | + | t.realTiled = !rp.Floating | |||
| 104 | + | t.client = c.clientRect(rp) | |||
| 105 | + | } | |||
| 106 | + | } | |||
| 107 | + | cp, t := c.captureConfigure(sc, ps, t) | |||
| 108 | + | if v, send := c.configures.nextWithCapture(ps, t, cp); send { | |||
| 109 | + | if err := c.command(ctx, v); err != nil { | |||
| 110 | + | return ports.Scene{}, false, err | |||
| 111 | + | } | |||
| 112 | + | c.configures.mark(v) | |||
| 113 | + | } | |||
| 114 | + | } | |||
| 115 | + | scene.Windows = append(scene.Windows, c.scenePopups(sc)...) | |||
| 116 | + | scene.CaptureIndicators = c.captureIndicators(sc) | |||
| 117 | + | scene.Capture = c.captureSceneFor(sc, capture) | |||
| 118 | + | // A scene carrying a capture image always gets a fresh Seq, and so | |||
| 119 | + | // does one of an animating screen (the one being stepped, or any | |||
| 120 | + | // when none is): the outputs drop a scene they already show, and a | |||
| 121 | + | // spring that starts at the previous scene, or rounds to it in its | |||
| 122 | + | // tail, would get no flip and wait for the fallback timer. Any other | |||
| 123 | + | // keeps its output's Seq while it draws the same, so an idle output | |||
| 124 | + | // is not recomposed. | |||
| 125 | + | withCapture := scene.Capture != nil && (sc == capture.hiddenScr || sc == capture.windowScr) | |||
| 126 | + | animating := (sc.springing() || c.pulsing(sc)) && (only == nil || only == sc) | |||
| 127 | + | if !withCapture && !animating && sc.last.Seq != 0 && scene.SameAs(sc.last) { | |||
| 128 | + | scene.Seq = sc.last.Seq | |||
| 129 | + | } else { | |||
| 130 | + | c.seq++ | |||
| 131 | + | scene.Seq = c.seq | |||
| 132 | + | } | |||
| 133 | + | if withCapture { | |||
| 134 | + | switch sc { | |||
| 135 | + | case capture.hiddenScr: | |||
| 136 | + | scene.CaptureScene = c.captureScene(scene.Seq) | |||
| 137 | + | case capture.windowScr: | |||
| 138 | + | scene.CaptureScene = c.captureWindowScene(capture, scene.Seq) | |||
| 139 | + | } | |||
| 140 | + | } | |||
| 141 | + | return scene, drawable, nil | |||
| 142 | + | } | |||