feat(screenshot): snap to active popouts & modals in region mode (#3618)

* feat(screenshot): snap to active popouts and modals in region mode

Allow region screenshot selector to detect and snap to visible shell
popouts and modals on hover and click or keyboard confirmation.

- PopoutManager.qml: add getActiveSurfaces() collecting visible
  DankPopout and DankModal global bounding boxes
- DMSShellIPC.qml: add screenshot.getSurfaces IPC endpoint
- core: fetch snap targets from shell IPC during region screenshot init
- selector: highlight snap targets on hover with surface name and dimensions
- selector: click or Enter/Space on hovered target locks selection to surface bounds
- selector: allow hover snap over pre-selection (SelectionState.fromPreSelect)
- selector: reset selection on degenerate (1x1) click release for quick re-selection
- unit tests for display name formatting, hover detection, and render bounds

* fix(screenshot): correct snap off-by-one and drag-away cancellation

- region_input: fix drag-away snap cancellation by setting hasSelection=true
  and clearing preSelect so freeform dragging renders and is not discarded
- region_input: add snapToTarget helper subtracting 1 device pixel (devPxX/Y)
  from currentX/currentY to prevent inclusive coordinate off-by-one (420x580 -> 420x580)
- region_render: compute hover bounds with w = round(width * scaleX) and
  x2 = x1 + w - 1 to eliminate +1 device pixel in render bounds
- region_snap_test: assert exact 420x580 bounds for both hover and snapped states

* style(screenshot): format region_snap_test.go with golangci-lint-fmt
This commit is contained in:
Huỳnh Thiện Lộc
2026-09-29 17:06:19 -04:00
committed by GitHub
parent c1818dee2c
commit 34e0ed2e90
8 changed files with 525 additions and 16 deletions
+31 -11
View File
@@ -9,11 +9,11 @@ import (
"os/exec"
"path/filepath"
"runtime/debug"
"strconv"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
"github.com/AvengeMedia/DankMaterialShell/core/internal/notify"
"github.com/AvengeMedia/DankMaterialShell/core/internal/qsipc"
"github.com/AvengeMedia/DankMaterialShell/core/internal/screenshot"
"github.com/spf13/cobra"
)
@@ -246,9 +246,29 @@ func getScreenshotConfig(mode screenshot.Mode) screenshot.Config {
}
config.Quality = ssQuality
if mode == screenshot.ModeRegion || mode == screenshot.ModeScroll {
config.SnapTargets = fetchShellSnapTargets()
}
return config
}
func fetchShellSnapTargets() []screenshot.SnapTarget {
pid, ok := shellApp.SessionPID()
if !ok {
return nil
}
res, isVoid, err := qsipc.Call(qsipc.SocketPathForPID(pid), "screenshot", "getSurfaces", nil)
if err != nil || isVoid || res == "" {
return nil
}
var targets []screenshot.SnapTarget
if err := json.Unmarshal([]byte(res), &targets); err != nil {
return nil
}
return targets
}
// setPopoutScreenshotMode toggles the shell handshake so popouts drop their keyboard grab during region select.
// Best-effort and not awaited: qs takes ~20ms to come up, longer than the selector needs to show.
func setPopoutScreenshotMode(begin bool) {
@@ -256,18 +276,18 @@ func setPopoutScreenshotMode(begin bool) {
if begin {
fn = "begin"
}
cmdArgs := []string{"ipc"}
if pid, ok := shellApp.SessionPID(); ok {
cmdArgs = append(cmdArgs, "--pid", strconv.Itoa(pid))
} else {
if err := shellApp.ResolveConfig(nil, nil); err != nil {
return
}
if qsHasAnyDisplay() {
cmdArgs = append(cmdArgs, "--any-display")
}
cmdArgs = append(cmdArgs, "-p", shellApp.ConfigPath())
_, _, _ = qsipc.Call(qsipc.SocketPathForPID(pid), "screenshot", fn, nil)
return
}
cmdArgs := []string{"ipc"}
if err := shellApp.ResolveConfig(nil, nil); err != nil {
return
}
if qsHasAnyDisplay() {
cmdArgs = append(cmdArgs, "--any-display")
}
cmdArgs = append(cmdArgs, "-p", shellApp.ConfigPath())
cmdArgs = append(cmdArgs, "call", "screenshot", fn)
_ = exec.Command("qs", cmdArgs...).Start()
}
+12 -3
View File
@@ -32,9 +32,10 @@ const (
)
type SelectionState struct {
hasSelection bool // There's a selection to display (pre-loaded or user-drawn)
dragging bool // User is actively drawing a new selection
surface *OutputSurface // Surface where selection was made
hasSelection bool // There's a selection to display (pre-loaded or user-drawn)
fromPreSelect bool // hasSelection was set by applyPreSelection, not by user interaction
dragging bool // User is actively drawing a new selection
surface *OutputSurface // Surface where selection was made
// Global logical coordinates. Keeping these independent of the active
// surface lets a drag continue across output boundaries.
anchorX float64
@@ -161,6 +162,12 @@ type RegionSelector struct {
phase selectorPhase
scroll *scrollSession
snapTargets []SnapTarget
hoveredTarget *SnapTarget
clickedTarget *SnapTarget
dragStartX float64
dragStartY float64
running bool
cancelled bool
result Region
@@ -175,6 +182,7 @@ func NewRegionSelector(s *Screenshoter) *RegionSelector {
outputs: make(map[uint32]*WaylandOutput),
preCapture: make(map[*WaylandOutput]*PreCapture),
showCapturedCursor: s.config.Cursor == CursorOn,
snapTargets: s.config.SnapTargets,
}
}
@@ -983,6 +991,7 @@ func (r *RegionSelector) applyPreSelection(os *OutputSurface) {
y2 := float64(r.preSelect.Y-os.output.y+r.preSelect.Height)*scaleY - scaleY
r.selection.hasSelection = true
r.selection.fromPreSelect = true
r.selection.dragging = false
r.selection.surface = os
r.selection.anchorX = float64(os.output.x) + x1
+122 -1
View File
@@ -53,6 +53,8 @@ func (r *RegionSelector) setupPointerHandlers() {
r.pointerY = e.SurfaceY
if r.selection.dragging {
r.updateSelectionCurrent(r.activeSurface, r.pointerX, r.pointerY)
} else {
r.updateHoverTarget(r.pointerX, r.pointerY)
}
r.refreshCursor()
})
@@ -74,9 +76,26 @@ func (r *RegionSelector) setupPointerHandlers() {
if r.ctrlHeld && r.selection.hasSelection {
r.refreshCursor()
}
r.updateHoverTarget(e.SurfaceX, e.SurfaceY)
return
}
if r.clickedTarget != nil && r.activeSurface.output != nil {
pointerX := e.SurfaceX + float64(r.activeSurface.output.x)
pointerY := e.SurfaceY + float64(r.activeSurface.output.y)
if math.Hypot(pointerX-r.dragStartX, pointerY-r.dragStartY) > 5 {
r.clickedTarget = nil
r.hoveredTarget = nil
r.preSelect = Region{}
r.selection.hasSelection = true
r.selection.anchorX = r.dragStartX
r.selection.anchorY = r.dragStartY
for _, os := range r.surfaces {
r.redrawSurface(os)
}
}
}
r.updateSelectionCurrent(r.activeSurface, e.SurfaceX, e.SurfaceY)
})
@@ -105,6 +124,8 @@ func (r *RegionSelector) setupPointerHandlers() {
case 1: // pressed
pointerX := r.pointerX + float64(r.activeSurface.output.x)
pointerY := r.pointerY + float64(r.activeSurface.output.y)
r.dragStartX = pointerX
r.dragStartY = pointerY
if r.ctrlHeld && r.selection.hasSelection {
if handle := r.resizeHandleAt(pointerX, pointerY); handle != handleNone {
if r.beginSelectionResize(handle, pointerX, pointerY) {
@@ -119,10 +140,23 @@ func (r *RegionSelector) setupPointerHandlers() {
}
}
if (!r.selection.hasSelection || r.selection.fromPreSelect) && r.hoveredTarget != nil {
r.clickedTarget = r.hoveredTarget
r.selection.dragging = true
r.selection.fromPreSelect = false
r.selection.surface = r.activeSurface
r.selection.anchorX = pointerX
r.selection.anchorY = pointerY
r.selection.currentX = pointerX
r.selection.currentY = pointerY
break
}
r.preSelect = Region{}
r.movingSelection = false
r.resizingHandle = handleNone
r.selection.hasSelection = true
r.selection.fromPreSelect = false
r.selection.dragging = true
r.selection.surface = r.activeSurface
r.selection.anchorX = pointerX
@@ -134,10 +168,41 @@ func (r *RegionSelector) setupPointerHandlers() {
r.redrawSurface(os)
}
case 0: // released
if r.clickedTarget != nil {
t := r.clickedTarget
r.clickedTarget = nil
r.hoveredTarget = nil
r.snapToTarget(t)
r.refreshCursor()
for _, os := range r.surfaces {
r.redrawSurface(os)
}
if r.screenshoter != nil && r.screenshoter.config.NoConfirm {
r.finishSelection()
}
break
}
r.selection.dragging = false
r.movingSelection = false
r.resizingHandle = handleNone
r.refreshCursor()
// A degenerate (1×1 or smaller) selection resets state so the
// user can re-draw without relaunching the command.
if r.selection.hasSelection {
if _, _, _, w, h := r.selectionDeviceRect(); w <= 1 && h <= 1 {
r.preSelect = Region{}
r.selection = SelectionState{}
r.hoveredTarget = nil
r.clickedTarget = nil
for _, os := range r.surfaces {
r.redrawSurface(os)
}
break
}
}
for _, os := range r.surfaces {
r.redrawSurface(os)
}
@@ -152,6 +217,57 @@ func (r *RegionSelector) setupPointerHandlers() {
})
}
func (r *RegionSelector) updateHoverTarget(surfaceX, surfaceY float64) {
// Block hover snap only when the user has actively drawn/committed a selection.
// A preloaded selection (fromPreSelect) does not block hover snapping.
if (r.selection.hasSelection && !r.selection.fromPreSelect) || r.activeSurface == nil || r.activeSurface.output == nil {
if r.hoveredTarget != nil {
r.hoveredTarget = nil
for _, os := range r.surfaces {
r.redrawSurface(os)
}
}
return
}
gx := surfaceX + float64(r.activeSurface.output.x)
gy := surfaceY + float64(r.activeSurface.output.y)
var hit *SnapTarget
for i := len(r.snapTargets) - 1; i >= 0; i-- {
t := &r.snapTargets[i]
if gx >= t.X && gx < t.X+t.Width && gy >= t.Y && gy < t.Y+t.Height {
hit = t
break
}
}
if hit != r.hoveredTarget {
r.hoveredTarget = hit
for _, os := range r.surfaces {
r.redrawSurface(os)
}
}
}
func (r *RegionSelector) snapToTarget(t *SnapTarget) {
r.preSelect = Region{}
os := r.activeSurface
devPxX, devPxY := 1.0, 1.0
if os != nil && os.screenBuf != nil && os.logicalW > 0 && os.logicalH > 0 {
devPxX = float64(os.logicalW) / float64(os.screenBuf.Width)
devPxY = float64(os.logicalH) / float64(os.screenBuf.Height)
}
r.selection = SelectionState{
hasSelection: true,
surface: os,
anchorX: t.X,
anchorY: t.Y,
currentX: t.X + t.Width - devPxX,
currentY: t.Y + t.Height - devPxY,
}
}
func (r *RegionSelector) setupTouchHandlers() {
r.touch.SetDownHandler(func(e client.TouchDownEvent) {
r.handleTouchDown(e.Surface, e.Id, e.X, e.Y)
@@ -626,7 +742,12 @@ func (r *RegionSelector) handleKey(sym string, state uint32) {
r.redrawSurface(os)
}
case "Return", "space", "KP_Enter":
if r.selection.hasSelection {
if r.hoveredTarget != nil && r.activeSurface != nil && (!r.selection.hasSelection || r.selection.fromPreSelect) {
t := r.hoveredTarget
r.hoveredTarget = nil
r.snapToTarget(t)
r.finishSelection()
} else if r.selection.hasSelection {
r.finishSelection()
}
}
+54
View File
@@ -67,6 +67,13 @@ var fontGlyphs = map[rune][fontCharH]uint8{
'/': {0x00, 0x02, 0x06, 0x0C, 0x18, 0x18, 0x30, 0x60, 0x40, 0x00, 0x00, 0x00},
'[': {0x3C, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x3C, 0x00, 0x00},
']': {0x3C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x3C, 0x00, 0x00},
'b': {0x00, 0x60, 0x60, 0x60, 0x7C, 0x66, 0x66, 0x66, 0x66, 0x7C, 0x00, 0x00},
'f': {0x00, 0x0E, 0x18, 0x18, 0x7E, 0x18, 0x18, 0x18, 0x18, 0x18, 0x00, 0x00},
'j': {0x00, 0x0C, 0x00, 0x1C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x4C, 0x38, 0x00},
'k': {0x00, 0x60, 0x60, 0x60, 0x66, 0x6C, 0x78, 0x6C, 0x66, 0x66, 0x00, 0x00},
'q': {0x00, 0x00, 0x00, 0x3E, 0x66, 0x66, 0x66, 0x3E, 0x06, 0x06, 0x00, 0x00},
'y': {0x00, 0x00, 0x00, 0x66, 0x66, 0x66, 0x66, 0x3E, 0x06, 0x7C, 0x00, 0x00},
'-': {0x00, 0x00, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
}
type OverlayStyle struct {
@@ -299,6 +306,53 @@ func (r *RegionSelector) selectionRenderBounds(os *OutputSurface) (selectionRend
return selectionRenderBounds{}, false
}
if (!r.selection.hasSelection || r.selection.fromPreSelect) && r.hoveredTarget != nil {
t := r.hoveredTarget
srcBuf := r.getSourceBuffer(os)
if srcBuf == nil {
return selectionRenderBounds{}, false
}
outputMinX := float64(os.output.x)
outputMinY := float64(os.output.y)
outputMaxX := outputMinX + float64(os.logicalW)
outputMaxY := outputMinY + float64(os.logicalH)
minX := t.X
minY := t.Y
maxX := t.X + t.Width
maxY := t.Y + t.Height
if math.Max(minX, outputMinX) >= math.Min(maxX, outputMaxX) ||
math.Max(minY, outputMinY) >= math.Min(maxY, outputMaxY) {
return selectionRenderBounds{}, false
}
scaleX, scaleY := 1.0, 1.0
if os.logicalW > 0 && os.logicalH > 0 {
scaleX = float64(srcBuf.Width) / float64(os.logicalW)
scaleY = float64(srcBuf.Height) / float64(os.logicalH)
}
x1 := clamp(int(math.Floor((minX-outputMinX)*scaleX)), 0, srcBuf.Width-1)
y1 := clamp(int(math.Floor((minY-outputMinY)*scaleY)), 0, srcBuf.Height-1)
w := int(math.Round(t.Width * scaleX))
h := int(math.Round(t.Height * scaleY))
x2 := clamp(x1+w-1, 0, srcBuf.Width-1)
y2 := clamp(y1+h-1, 0, srcBuf.Height-1)
w = x2 - x1 + 1
h = y2 - y1 + 1
labelText := fmt.Sprintf("[%s] %dx%d", t.DisplayName(), w, h)
return selectionRenderBounds{
x: x1, y: y1, w: w, h: h, labelText: labelText,
top: minY >= outputMinY && minY < outputMaxY,
bottom: maxY > outputMinY && maxY <= outputMaxY,
left: minX >= outputMinX && minX < outputMaxX,
right: maxX > outputMinX && maxX <= outputMaxX,
scaleX: scaleX,
}, true
}
ext, ok := r.selectionExtent()
if !ok || !ext.intersects(os) {
return selectionRenderBounds{}, false
@@ -0,0 +1,204 @@
package screenshot
import (
"testing"
)
func TestSnapTargetDisplayName(t *testing.T) {
tests := []struct {
target SnapTarget
expected string
}{
{
target: SnapTarget{Name: "dms:control-center", Type: "popout"},
expected: "control center",
},
{
target: SnapTarget{Name: "dms:clipboard-popout", Type: "popout"},
expected: "clipboard",
},
{
target: SnapTarget{Name: "dms:wifi-password", Type: "modal"},
expected: "wifi password",
},
{
target: SnapTarget{Name: "dms:polkit-auth-surface", Type: "modal"},
expected: "polkit auth",
},
{
target: SnapTarget{Name: "", Type: "popout"},
expected: "popout",
},
}
for _, tt := range tests {
got := tt.target.DisplayName()
if got != tt.expected {
t.Errorf("DisplayName() for %+v = %q, want %q", tt.target, got, tt.expected)
}
}
}
func TestUpdateHoverTarget(t *testing.T) {
mockSurface := &OutputSurface{
logicalW: 1920,
logicalH: 1080,
output: &WaylandOutput{
name: "eDP-1",
x: 0,
y: 0,
fractionalScale: 1.0,
},
screenBuf: &ShmBuffer{
Width: 1920,
Height: 1080,
Stride: 1920 * 4,
},
}
targets := []SnapTarget{
{
Name: "dms:control-center",
Type: "popout",
X: 1400,
Y: 48,
Width: 420,
Height: 580,
},
{
Name: "dms:clipboard-popout",
Type: "popout",
X: 500,
Y: 100,
Width: 350,
Height: 400,
},
}
r := &RegionSelector{
activeSurface: mockSurface,
surfaces: []*OutputSurface{mockSurface},
snapTargets: targets,
}
// 1. Outside any target
r.updateHoverTarget(100, 100)
if r.hoveredTarget != nil {
t.Fatalf("expected nil hoveredTarget, got %+v", r.hoveredTarget)
}
// 2. Inside control center
r.updateHoverTarget(1500, 100)
if r.hoveredTarget == nil || r.hoveredTarget.Name != "dms:control-center" {
t.Fatalf("expected control-center target, got %+v", r.hoveredTarget)
}
// 3. Move inside clipboard
r.updateHoverTarget(600, 200)
if r.hoveredTarget == nil || r.hoveredTarget.Name != "dms:clipboard-popout" {
t.Fatalf("expected clipboard target, got %+v", r.hoveredTarget)
}
// 4. Move outside
r.updateHoverTarget(0, 0)
if r.hoveredTarget != nil {
t.Fatalf("expected nil hoveredTarget when outside, got %+v", r.hoveredTarget)
}
// 5. When user has actively drawn a selection, hoverTarget must not change
r.selection.hasSelection = true
r.selection.fromPreSelect = false
r.updateHoverTarget(1500, 100)
if r.hoveredTarget != nil {
t.Fatalf("expected nil hoveredTarget when user selection is active, got %+v", r.hoveredTarget)
}
// 6. When selection is a preselection (fromPreSelect=true), hover snap must still work
r.selection.hasSelection = true
r.selection.fromPreSelect = true
r.updateHoverTarget(1500, 100)
if r.hoveredTarget == nil || r.hoveredTarget.Name != "dms:control-center" {
t.Fatalf("expected control-center hover over preselection, got %+v", r.hoveredTarget)
}
}
func TestSelectionRenderBoundsHoveredTarget(t *testing.T) {
mockSurface := &OutputSurface{
logicalW: 1920,
logicalH: 1080,
output: &WaylandOutput{
name: "eDP-1",
x: 0,
y: 0,
fractionalScale: 1.0,
},
screenBuf: &ShmBuffer{
Width: 1920,
Height: 1080,
Stride: 1920 * 4,
},
}
target := &SnapTarget{
Name: "dms:control-center",
Type: "popout",
X: 1400,
Y: 48,
Width: 420,
Height: 580,
}
r := &RegionSelector{
activeSurface: mockSurface,
surfaces: []*OutputSurface{mockSurface},
hoveredTarget: target,
}
bounds, ok := r.selectionRenderBounds(mockSurface)
if !ok {
t.Fatalf("expected selectionRenderBounds to return ok for hovered target")
}
if bounds.x != 1400 || bounds.y != 48 || bounds.w != 420 || bounds.h != 580 {
t.Errorf("unexpected bounds: %+v", bounds)
}
if bounds.labelText != "[control center] 420x580" {
t.Errorf("unexpected label text: %q", bounds.labelText)
}
// With a preselection active, hoveredTarget must still be rendered.
r.selection.hasSelection = true
r.selection.fromPreSelect = true
bounds, ok = r.selectionRenderBounds(mockSurface)
if !ok {
t.Fatalf("expected selectionRenderBounds to return ok over preselection")
}
if bounds.x != 1400 || bounds.y != 48 || bounds.w != 420 || bounds.h != 580 {
t.Errorf("unexpected bounds over preselection: %+v", bounds)
}
// With a user-drawn selection, hoveredTarget must NOT be rendered.
r.selection.fromPreSelect = false
_, ok = r.selectionRenderBounds(mockSurface)
if ok {
t.Fatalf("expected selectionRenderBounds to return !ok when user selection is active")
}
// Snapping to target must produce exact target dimensions without off-by-one.
r.hoveredTarget = nil
r.snapToTarget(target)
if !r.selection.hasSelection || r.selection.fromPreSelect {
t.Fatalf("expected committed selection from snapToTarget")
}
snappedBounds, ok := r.selectionRenderBounds(mockSurface)
if !ok {
t.Fatalf("expected selectionRenderBounds to return ok for snapped target")
}
if snappedBounds.x != 1400 || snappedBounds.y != 48 || snappedBounds.w != 420 || snappedBounds.h != 580 {
t.Errorf("unexpected snapped bounds: %+v (want 1400, 48, 420, 580)", snappedBounds)
}
if snappedBounds.labelText != "420x580" {
t.Errorf("unexpected snapped label text: %q (want 420x580)", snappedBounds.labelText)
}
}
+29 -1
View File
@@ -1,6 +1,9 @@
package screenshot
import "fmt"
import (
"fmt"
"strings"
)
type Mode int
@@ -55,6 +58,30 @@ type Output struct {
Transform int32
}
type SnapTarget struct {
Name string `json:"name"`
Type string `json:"type"`
X float64 `json:"x"`
Y float64 `json:"y"`
Width float64 `json:"width"`
Height float64 `json:"height"`
}
func (s SnapTarget) DisplayName() string {
name := strings.TrimPrefix(s.Name, "dms:")
for _, suffix := range []string{"-popout", "-modal", "-surface"} {
name = strings.TrimSuffix(name, suffix)
}
name = strings.ReplaceAll(name, "-", " ")
if name == "" {
if s.Type != "" {
return s.Type
}
return "surface"
}
return name
}
type Config struct {
Seat string
Mode Mode
@@ -74,6 +101,7 @@ type Config struct {
AllowMultiple bool
IntervalMs int
HUD string
SnapTargets []SnapTarget
// SelectorHook runs as the interactive selector starts (true) and ends (false).
SelectorHook func(begin bool)
}
+69
View File
@@ -337,4 +337,73 @@ Singleton {
function requestHoverPopout(popout, tabIndex, triggerSource) {
_requestPopout(popout, tabIndex, triggerSource, true);
}
function getActiveSurfaces() {
const results = [];
for (const screenName in currentPopoutsByScreen) {
const popout = currentPopoutsByScreen[screenName];
if (!popout || !popout.shouldBeVisible)
continue;
const host = popout.impl?.item ?? popout;
if (!host || !host.shouldBeVisible || host.isClosing)
continue;
const screen = host.surfaceScreen ?? popout.screen ?? null;
const sx = screen?.x ?? 0;
const sy = screen?.y ?? 0;
const px = host.renderedAlignedX;
const py = host.renderedAlignedY;
const pw = host.renderedAlignedWidth ?? popout.popupWidth ?? 0;
const ph = host.renderedAlignedHeight ?? popout.popupHeight ?? 0;
// Skip if origin is not yet laid out (undefined/null means not positioned).
if (px == null || py == null || pw <= 0 || ph <= 0)
continue;
results.push({
"name": popout.layerNamespace || "popout",
"type": "popout",
"x": Math.round(sx + px),
"y": Math.round(sy + py),
"width": Math.round(pw),
"height": Math.round(ph)
});
}
const modals = ModalManager.currentModalsByScreen || {};
for (const screenName in modals) {
const modal = modals[screenName];
if (!modal || !modal.shouldBeVisible)
continue;
const host = modal.impl?.item ?? modal;
if (!host || !host.shouldBeVisible || host.isClosing)
continue;
const screen = host.effectiveScreen ?? modal.targetScreen ?? null;
const sx = screen?.x ?? 0;
const sy = screen?.y ?? 0;
const mx = host.alignedX;
const my = host.alignedY;
const mw = host.alignedWidth ?? modal.modalWidth ?? 0;
const mh = host.alignedHeight ?? modal.modalHeight ?? 0;
// Skip if origin is not yet laid out.
if (mx == null || my == null || mw <= 0 || mh <= 0)
continue;
results.push({
"name": modal.layerNamespace || "modal",
"type": "modal",
"x": Math.round(sx + mx),
"y": Math.round(sy + my),
"width": Math.round(mw),
"height": Math.round(mh)
});
}
return results;
}
}
+4
View File
@@ -325,6 +325,10 @@ Item {
return "SCREENSHOT_MODE_OFF";
}
function getSurfaces(): string {
return JSON.stringify(PopoutManager.getActiveSurfaces());
}
target: "screenshot"
}