1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-31 08:52:49 -05:00

Compare commits

...

16 Commits

Author SHA1 Message Date
bbedward
deaac3fdf0 list: approve mouse detection 2025-12-08 14:11:44 -05:00
bbedward
b7062fe40c windows: dont close on esc
fixes #911
2025-12-08 14:02:58 -05:00
bbedward
64d5e99b9d dock: ensure creation after bars
fixes #919
2025-12-08 13:54:44 -05:00
bbedward
f9d8a7d22b greeter: fix weather setting
fixes #921
2025-12-08 13:45:26 -05:00
bbedward
52fcd3ad98 lock: make VPN icon white to be consistent with others
fixes #926
2025-12-08 13:24:53 -05:00
bbedward
9d1e0ee29b fix color picker color space 2025-12-08 12:59:24 -05:00
bbedward
de62f48f50 screenshot: handle transformed displays 2025-12-08 12:45:05 -05:00
bbedward
f47b19274c media: fix position/bar awareness
- shift media control column so it doesnt go off screen
fixes #942
2025-12-08 11:51:40 -05:00
bbedward
bb7f7083b9 meta: transparency fixes
- fixes #949 - transparency not working > 95%
- fixes #947 - dont apply opacity to windows, defer to window-rules
2025-12-08 11:43:29 -05:00
Yuxiang Qiu
cd580090dc evdev: improve capslock detection for no led device (#923)
* evdev: improve capslock detection for no led device

* style: fmt
2025-12-08 11:16:43 -05:00
Marcus Ramberg
ddb74b598d ci: add flake check (#951) 2025-12-08 11:15:35 -05:00
bbedward
29571fc3aa screenshot: use wlr-output-management on DWL for x/y offsets 2025-12-08 10:53:08 -05:00
bbedward
57ee0fb2bd bump: failed fprint tries 2025-12-08 10:02:53 -05:00
osscar
3ef10e73a5 nix: remove leading dot in nativeBuildInputs (#948)
Co-authored-by: osscar <osscar.unheard025@passmail.net>
2025-12-08 15:52:32 +01:00
bbedward
dc40492fc7 cc: fix audio slider binding 2025-12-08 09:45:25 -05:00
bbedward
e606a76a86 screenshot: add screenshot-window support for DWL/MangoWC 2025-12-08 09:39:42 -05:00
43 changed files with 1314 additions and 327 deletions

30
.github/workflows/nix-pr-check.yml vendored Normal file
View File

@@ -0,0 +1,30 @@
name: Check nix flake
on:
pull_request:
branches: [master, main]
paths:
- "flake.*"
- "distro/nix/**"
jobs:
check-flake:
runs-on: ubuntu-latest
steps:
- name: Create GitHub App token
id: app_token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ steps.app_token.outputs.token }}
- name: Install Nix
uses: cachix/install-nix-action@v31
- name: Update vendorHash in flake.nix
run: nix flake check

View File

@@ -35,7 +35,7 @@ Modes:
full - Capture the focused output full - Capture the focused output
all - Capture all outputs combined all - Capture all outputs combined
output - Capture a specific output by name output - Capture a specific output by name
window - Capture the focused window (Hyprland only) window - Capture the focused window (Hyprland/DWL)
last - Capture the last selected region last - Capture the last selected region
Output format (--format): Output format (--format):
@@ -91,8 +91,7 @@ If no previous region exists, falls back to interactive selection.`,
var ssWindowCmd = &cobra.Command{ var ssWindowCmd = &cobra.Command{
Use: "window", Use: "window",
Short: "Capture the focused window", Short: "Capture the focused window",
Long: `Capture the currently focused window. Long: `Capture the currently focused window. Supported on Hyprland and DWL.`,
Currently only supported on Hyprland.`,
Run: runScreenshotWindow, Run: runScreenshotWindow,
} }
@@ -296,7 +295,14 @@ func bufferToRGBThumbnail(buf *screenshot.ShmBuffer, maxSize int, pixelFormat ui
data := buf.Data() data := buf.Data()
rgb := make([]byte, dstW*dstH*3) rgb := make([]byte, dstW*dstH*3)
swapRB := pixelFormat == uint32(screenshot.FormatARGB8888) || pixelFormat == uint32(screenshot.FormatXRGB8888) || pixelFormat == 0
var swapRB bool
switch pixelFormat {
case uint32(screenshot.FormatABGR8888), uint32(screenshot.FormatXBGR8888):
swapRB = false
default:
swapRB = true
}
for y := 0; y < dstH; y++ { for y := 0; y < dstH; y++ {
srcY := int(float64(y) / scale) srcY := int(float64(y) / scale)
@@ -310,7 +316,9 @@ func bufferToRGBThumbnail(buf *screenshot.ShmBuffer, maxSize int, pixelFormat ui
} }
si := srcY*buf.Stride + srcX*4 si := srcY*buf.Stride + srcX*4
di := (y*dstW + x) * 3 di := (y*dstW + x) * 3
if si+2 < len(data) { if si+3 >= len(data) {
continue
}
if swapRB { if swapRB {
rgb[di+0] = data[si+2] rgb[di+0] = data[si+2]
rgb[di+1] = data[si+1] rgb[di+1] = data[si+1]
@@ -322,7 +330,6 @@ func bufferToRGBThumbnail(buf *screenshot.ShmBuffer, maxSize int, pixelFormat ui
} }
} }
} }
}
return rgb, dstW, dstH return rgb, dstW, dstH
} }
@@ -371,7 +378,37 @@ func runScreenshotList(cmd *cobra.Command, args []string) {
} }
for _, o := range outputs { for _, o := range outputs {
fmt.Printf("%s: %dx%d+%d+%d (scale: %d)\n", scaleStr := fmt.Sprintf("%.2f", o.FractionalScale)
o.Name, o.Width, o.Height, o.X, o.Y, o.Scale) if o.FractionalScale == float64(int(o.FractionalScale)) {
scaleStr = fmt.Sprintf("%d", int(o.FractionalScale))
}
transformStr := transformName(o.Transform)
fmt.Printf("%s: %dx%d+%d+%d scale=%s transform=%s\n",
o.Name, o.Width, o.Height, o.X, o.Y, scaleStr, transformStr)
}
}
func transformName(t int32) string {
switch t {
case 0:
return "normal"
case 1:
return "90"
case 2:
return "180"
case 3:
return "270"
case 4:
return "flipped"
case 5:
return "flipped-90"
case 6:
return "flipped-180"
case 7:
return "flipped-270"
default:
return fmt.Sprintf("%d", t)
} }
} }

View File

@@ -1,6 +1,7 @@
package colorpicker package colorpicker
import ( import (
"fmt"
"math" "math"
"strings" "strings"
"sync" "sync"
@@ -79,6 +80,10 @@ func (s *SurfaceState) OnScreencopyBuffer(format PixelFormat, width, height, str
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()
if stride < width*4 {
return fmt.Errorf("invalid stride %d for width %d", stride, width)
}
if s.screenBuf != nil { if s.screenBuf != nil {
s.screenBuf.Close() s.screenBuf.Close()
s.screenBuf = nil s.screenBuf = nil
@@ -279,10 +284,10 @@ func (s *SurfaceState) Redraw() *ShmBuffer {
drawMagnifierWithInversion( drawMagnifierWithInversion(
dst.Data(), dst.Stride, dst.Width, dst.Height, dst.Data(), dst.Stride, dst.Width, dst.Height,
s.screenBuf.Data(), s.screenBuf.Stride, s.screenBuf.Width, s.screenBuf.Height, s.screenBuf.Data(), s.screenBuf.Stride, s.screenBuf.Width, s.screenBuf.Height,
px, py, picked, s.yInverted, px, py, picked, s.yInverted, s.screenFormat,
) )
drawColorPreview(dst.Data(), dst.Stride, dst.Width, dst.Height, px, py, picked, s.displayFormat, s.lowercase) drawColorPreview(dst.Data(), dst.Stride, dst.Width, dst.Height, px, py, picked, s.displayFormat, s.lowercase, s.screenFormat)
return dst return dst
} }
@@ -390,6 +395,7 @@ func drawMagnifierWithInversion(
cx, cy int, cx, cy int,
borderColor Color, borderColor Color,
yInverted bool, yInverted bool,
format PixelFormat,
) { ) {
if dstW <= 0 || dstH <= 0 || srcW <= 0 || srcH <= 0 { if dstW <= 0 || dstH <= 0 || srcW <= 0 || srcH <= 0 {
return return
@@ -407,6 +413,14 @@ func drawMagnifierWithInversion(
innerRadius := float64(outerRadius - borderThickness) innerRadius := float64(outerRadius - borderThickness)
outerRadiusF := float64(outerRadius) outerRadiusF := float64(outerRadius)
var rOff, bOff int
switch format {
case FormatABGR8888, FormatXBGR8888:
rOff, bOff = 0, 2
default:
rOff, bOff = 2, 0
}
for dy := -outerRadius - 2; dy <= outerRadius+2; dy++ { for dy := -outerRadius - 2; dy <= outerRadius+2; dy++ {
y := cy + dy y := cy + dy
if y < 0 || y >= dstH { if y < 0 || y >= dstH {
@@ -431,9 +445,9 @@ func drawMagnifierWithInversion(
} }
bgColor := Color{ bgColor := Color{
B: dst[dstOff+0], R: dst[dstOff+rOff],
G: dst[dstOff+1], G: dst[dstOff+1],
R: dst[dstOff+2], B: dst[dstOff+bOff],
A: dst[dstOff+3], A: dst[dstOff+3],
} }
@@ -462,7 +476,7 @@ func drawMagnifierWithInversion(
} }
srcOff := sy*srcStride + sx*4 srcOff := sy*srcStride + sx*4
if srcOff+4 <= len(src) { if srcOff+4 <= len(src) {
magColor := Color{B: src[srcOff+0], G: src[srcOff+1], R: src[srcOff+2], A: 255} magColor := Color{R: src[srcOff+rOff], G: src[srcOff+1], B: src[srcOff+bOff], A: 255}
finalColor = blendColors(magColor, borderColor, alpha) finalColor = blendColors(magColor, borderColor, alpha)
} else { } else {
finalColor = borderColor finalColor = borderColor
@@ -483,24 +497,25 @@ func drawMagnifierWithInversion(
} }
srcOff := sy*srcStride + sx*4 srcOff := sy*srcStride + sx*4
if srcOff+4 <= len(src) { if srcOff+4 <= len(src) {
finalColor = Color{B: src[srcOff+0], G: src[srcOff+1], R: src[srcOff+2], A: 255} finalColor = Color{R: src[srcOff+rOff], G: src[srcOff+1], B: src[srcOff+bOff], A: 255}
} else { } else {
continue continue
} }
} }
dst[dstOff+0] = finalColor.B dst[dstOff+rOff] = finalColor.R
dst[dstOff+1] = finalColor.G dst[dstOff+1] = finalColor.G
dst[dstOff+2] = finalColor.R dst[dstOff+bOff] = finalColor.B
dst[dstOff+3] = 255 dst[dstOff+3] = 255
} }
} }
drawMagnifierCrosshair(dst, dstStride, dstW, dstH, cx, cy, int(innerRadius), crossThickness, crossInnerRadius) drawMagnifierCrosshair(dst, dstStride, dstW, dstH, cx, cy, int(innerRadius), crossThickness, crossInnerRadius, format)
} }
func drawMagnifierCrosshair( func drawMagnifierCrosshair(
data []byte, stride, width, height, cx, cy, radius, thickness, innerRadius int, data []byte, stride, width, height, cx, cy, radius, thickness, innerRadius int,
format PixelFormat,
) { ) {
if width <= 0 || height <= 0 { if width <= 0 || height <= 0 {
return return
@@ -998,7 +1013,7 @@ var fontGlyphs = map[rune][fontH]uint8{
}, },
} }
func drawColorPreview(data []byte, stride, width, height int, cx, cy int, c Color, format OutputFormat, lowercase bool) { func drawColorPreview(data []byte, stride, width, height int, cx, cy int, c Color, format OutputFormat, lowercase bool, pixelFormat PixelFormat) {
text := formatColorForPreview(c, format, lowercase) text := formatColorForPreview(c, format, lowercase)
if len(text) == 0 { if len(text) == 0 {
return return
@@ -1033,9 +1048,8 @@ func drawColorPreview(data []byte, stride, width, height int, cx, cy int, c Colo
y = height - boxH y = height - boxH
} }
drawFilledRect(data, stride, width, height, x, y, boxW, boxH, c) drawFilledRect(data, stride, width, height, x, y, boxW, boxH, c, pixelFormat)
// Use contrasting text color based on luminance
lum := 0.299*float64(c.R) + 0.587*float64(c.G) + 0.114*float64(c.B) lum := 0.299*float64(c.R) + 0.587*float64(c.G) + 0.114*float64(c.B)
var fg Color var fg Color
if lum > 128 { if lum > 128 {
@@ -1043,7 +1057,7 @@ func drawColorPreview(data []byte, stride, width, height int, cx, cy int, c Colo
} else { } else {
fg = Color{R: 255, G: 255, B: 255, A: 255} fg = Color{R: 255, G: 255, B: 255, A: 255}
} }
drawText(data, stride, width, height, x+paddingX, y+paddingY, text, fg) drawText(data, stride, width, height, x+paddingX, y+paddingY, text, fg, pixelFormat)
} }
func formatColorForPreview(c Color, format OutputFormat, lowercase bool) string { func formatColorForPreview(c Color, format OutputFormat, lowercase bool) string {
@@ -1064,7 +1078,7 @@ func formatColorForPreview(c Color, format OutputFormat, lowercase bool) string
} }
} }
func drawFilledRect(data []byte, stride, width, height, x, y, w, h int, col Color) { func drawFilledRect(data []byte, stride, width, height, x, y, w, h int, col Color, format PixelFormat) {
if w <= 0 || h <= 0 { if w <= 0 || h <= 0 {
return return
} }
@@ -1073,6 +1087,14 @@ func drawFilledRect(data []byte, stride, width, height, x, y, w, h int, col Colo
x = clamp(x, 0, width) x = clamp(x, 0, width)
y = clamp(y, 0, height) y = clamp(y, 0, height)
var rOff, bOff int
switch format {
case FormatABGR8888, FormatXBGR8888:
rOff, bOff = 0, 2
default:
rOff, bOff = 2, 0
}
for yy := y; yy < yEnd; yy++ { for yy := y; yy < yEnd; yy++ {
rowOff := yy * stride rowOff := yy * stride
for xx := x; xx < xEnd; xx++ { for xx := x; xx < xEnd; xx++ {
@@ -1080,26 +1102,34 @@ func drawFilledRect(data []byte, stride, width, height, x, y, w, h int, col Colo
if off+4 > len(data) { if off+4 > len(data) {
continue continue
} }
data[off+0] = col.B data[off+rOff] = col.R
data[off+1] = col.G data[off+1] = col.G
data[off+2] = col.R data[off+bOff] = col.B
data[off+3] = 255 data[off+3] = 255
} }
} }
} }
func drawText(data []byte, stride, width, height, x, y int, text string, col Color) { func drawText(data []byte, stride, width, height, x, y int, text string, col Color, format PixelFormat) {
for i, r := range text { for i, r := range text {
drawGlyph(data, stride, width, height, x+i*(fontW+2), y, r, col) drawGlyph(data, stride, width, height, x+i*(fontW+2), y, r, col, format)
} }
} }
func drawGlyph(data []byte, stride, width, height, x, y int, r rune, col Color) { func drawGlyph(data []byte, stride, width, height, x, y int, r rune, col Color, format PixelFormat) {
g, ok := fontGlyphs[r] g, ok := fontGlyphs[r]
if !ok { if !ok {
return return
} }
var rOff, bOff int
switch format {
case FormatABGR8888, FormatXBGR8888:
rOff, bOff = 0, 2
default:
rOff, bOff = 2, 0
}
for row := 0; row < fontH; row++ { for row := 0; row < fontH; row++ {
yy := y + row yy := y + row
if yy < 0 || yy >= height { if yy < 0 || yy >= height {
@@ -1123,9 +1153,9 @@ func drawGlyph(data []byte, stride, width, height, x, y int, r rune, col Color)
continue continue
} }
data[off+0] = col.B data[off+rOff] = col.R
data[off+1] = col.G data[off+1] = col.G
data[off+2] = col.R data[off+bOff] = col.B
data[off+3] = 255 data[off+3] = 255
} }
} }

View File

@@ -5,6 +5,11 @@ import (
"fmt" "fmt"
"os" "os"
"os/exec" "os/exec"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/dwl_ipc"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_output_management"
wlhelpers "github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/client"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
) )
type Compositor int type Compositor int
@@ -44,10 +49,42 @@ func DetectCompositor() Compositor {
return detectedCompositor return detectedCompositor
} }
if detectDWLProtocol() {
detectedCompositor = CompositorDWL
return detectedCompositor
}
detectedCompositor = CompositorUnknown detectedCompositor = CompositorUnknown
return detectedCompositor return detectedCompositor
} }
func detectDWLProtocol() bool {
display, err := client.Connect("")
if err != nil {
return false
}
ctx := display.Context()
defer ctx.Close()
registry, err := display.GetRegistry()
if err != nil {
return false
}
found := false
registry.SetGlobalHandler(func(e client.RegistryGlobalEvent) {
if e.Interface == dwl_ipc.ZdwlIpcManagerV2InterfaceName {
found = true
}
})
if err := wlhelpers.Roundtrip(display, ctx); err != nil {
return false
}
return found
}
func SetCompositorDWL() { func SetCompositorDWL() {
detectedCompositor = CompositorDWL detectedCompositor = CompositorDWL
} }
@@ -57,14 +94,21 @@ type WindowGeometry struct {
Y int32 Y int32
Width int32 Width int32
Height int32 Height int32
Output string
Scale float64
OutputX int32
OutputY int32
OutputTransform int32
} }
func GetActiveWindow() (*WindowGeometry, error) { func GetActiveWindow() (*WindowGeometry, error) {
switch DetectCompositor() { switch DetectCompositor() {
case CompositorHyprland: case CompositorHyprland:
return getHyprlandActiveWindow() return getHyprlandActiveWindow()
case CompositorDWL:
return getDWLActiveWindow()
default: default:
return nil, fmt.Errorf("window capture requires Hyprland") return nil, fmt.Errorf("window capture requires Hyprland or DWL")
} }
} }
@@ -220,7 +264,112 @@ func SetDWLActiveOutput(name string) {
} }
func getDWLFocusedMonitor() string { func getDWLFocusedMonitor() string {
if dwlActiveOutput != "" {
return dwlActiveOutput return dwlActiveOutput
}
return queryDWLActiveOutput()
}
func queryDWLActiveOutput() string {
display, err := client.Connect("")
if err != nil {
return ""
}
ctx := display.Context()
defer ctx.Close()
registry, err := display.GetRegistry()
if err != nil {
return ""
}
var dwlManager *dwl_ipc.ZdwlIpcManagerV2
outputs := make(map[uint32]*client.Output)
registry.SetGlobalHandler(func(e client.RegistryGlobalEvent) {
switch e.Interface {
case dwl_ipc.ZdwlIpcManagerV2InterfaceName:
mgr := dwl_ipc.NewZdwlIpcManagerV2(ctx)
if err := registry.Bind(e.Name, e.Interface, e.Version, mgr); err == nil {
dwlManager = mgr
}
case client.OutputInterfaceName:
out := client.NewOutput(ctx)
version := e.Version
if version > 4 {
version = 4
}
if err := registry.Bind(e.Name, e.Interface, version, out); err == nil {
outputs[e.Name] = out
}
}
})
if err := wlhelpers.Roundtrip(display, ctx); err != nil {
return ""
}
if dwlManager == nil || len(outputs) == 0 {
return ""
}
outputNames := make(map[uint32]string)
for name, out := range outputs {
n := name
out.SetNameHandler(func(e client.OutputNameEvent) {
outputNames[n] = e.Name
})
}
if err := wlhelpers.Roundtrip(display, ctx); err != nil {
return ""
}
type outputState struct {
name string
active bool
gotFrame bool
}
states := make(map[uint32]*outputState)
for name, out := range outputs {
dwlOut, err := dwlManager.GetOutput(out)
if err != nil {
continue
}
state := &outputState{name: outputNames[name]}
states[name] = state
dwlOut.SetActiveHandler(func(e dwl_ipc.ZdwlIpcOutputV2ActiveEvent) {
state.active = e.Active != 0
})
dwlOut.SetFrameHandler(func(e dwl_ipc.ZdwlIpcOutputV2FrameEvent) {
state.gotFrame = true
})
}
allFramesReceived := func() bool {
for _, s := range states {
if !s.gotFrame {
return false
}
}
return true
}
for !allFramesReceived() {
if err := ctx.Dispatch(); err != nil {
return ""
}
}
for _, state := range states {
if state.active {
return state.name
}
}
return ""
} }
func GetFocusedMonitor() string { func GetFocusedMonitor() string {
@@ -236,3 +385,238 @@ func GetFocusedMonitor() string {
} }
return "" return ""
} }
type outputInfo struct {
x, y int32
transform int32
}
func getOutputInfo(outputName string) (*outputInfo, bool) {
display, err := client.Connect("")
if err != nil {
return nil, false
}
ctx := display.Context()
defer ctx.Close()
registry, err := display.GetRegistry()
if err != nil {
return nil, false
}
var outputManager *wlr_output_management.ZwlrOutputManagerV1
registry.SetGlobalHandler(func(e client.RegistryGlobalEvent) {
if e.Interface == wlr_output_management.ZwlrOutputManagerV1InterfaceName {
mgr := wlr_output_management.NewZwlrOutputManagerV1(ctx)
version := e.Version
if version > 4 {
version = 4
}
if err := registry.Bind(e.Name, e.Interface, version, mgr); err == nil {
outputManager = mgr
}
}
})
if err := wlhelpers.Roundtrip(display, ctx); err != nil {
return nil, false
}
if outputManager == nil {
return nil, false
}
type headState struct {
name string
x, y int32
transform int32
}
heads := make(map[*wlr_output_management.ZwlrOutputHeadV1]*headState)
done := false
outputManager.SetHeadHandler(func(e wlr_output_management.ZwlrOutputManagerV1HeadEvent) {
state := &headState{}
heads[e.Head] = state
e.Head.SetNameHandler(func(ne wlr_output_management.ZwlrOutputHeadV1NameEvent) {
state.name = ne.Name
})
e.Head.SetPositionHandler(func(pe wlr_output_management.ZwlrOutputHeadV1PositionEvent) {
state.x = pe.X
state.y = pe.Y
})
e.Head.SetTransformHandler(func(te wlr_output_management.ZwlrOutputHeadV1TransformEvent) {
state.transform = te.Transform
})
})
outputManager.SetDoneHandler(func(e wlr_output_management.ZwlrOutputManagerV1DoneEvent) {
done = true
})
for !done {
if err := ctx.Dispatch(); err != nil {
return nil, false
}
}
for _, state := range heads {
if state.name == outputName {
return &outputInfo{
x: state.x,
y: state.y,
transform: state.transform,
}, true
}
}
return nil, false
}
func getDWLActiveWindow() (*WindowGeometry, error) {
display, err := client.Connect("")
if err != nil {
return nil, fmt.Errorf("connect: %w", err)
}
ctx := display.Context()
defer ctx.Close()
registry, err := display.GetRegistry()
if err != nil {
return nil, fmt.Errorf("get registry: %w", err)
}
var dwlManager *dwl_ipc.ZdwlIpcManagerV2
outputs := make(map[uint32]*client.Output)
registry.SetGlobalHandler(func(e client.RegistryGlobalEvent) {
switch e.Interface {
case dwl_ipc.ZdwlIpcManagerV2InterfaceName:
mgr := dwl_ipc.NewZdwlIpcManagerV2(ctx)
if err := registry.Bind(e.Name, e.Interface, e.Version, mgr); err == nil {
dwlManager = mgr
}
case client.OutputInterfaceName:
out := client.NewOutput(ctx)
version := e.Version
if version > 4 {
version = 4
}
if err := registry.Bind(e.Name, e.Interface, version, out); err == nil {
outputs[e.Name] = out
}
}
})
if err := wlhelpers.Roundtrip(display, ctx); err != nil {
return nil, fmt.Errorf("roundtrip: %w", err)
}
if dwlManager == nil {
return nil, fmt.Errorf("dwl_ipc_manager not available")
}
if len(outputs) == 0 {
return nil, fmt.Errorf("no outputs found")
}
outputNames := make(map[uint32]string)
for name, out := range outputs {
n := name
out.SetNameHandler(func(e client.OutputNameEvent) {
outputNames[n] = e.Name
})
}
if err := wlhelpers.Roundtrip(display, ctx); err != nil {
return nil, fmt.Errorf("roundtrip: %w", err)
}
type dwlOutputState struct {
output *dwl_ipc.ZdwlIpcOutputV2
name string
active bool
x, y int32
w, h int32
scalefactor uint32
gotFrame bool
}
dwlOutputs := make(map[uint32]*dwlOutputState)
for name, out := range outputs {
dwlOut, err := dwlManager.GetOutput(out)
if err != nil {
continue
}
state := &dwlOutputState{output: dwlOut, name: outputNames[name]}
dwlOutputs[name] = state
dwlOut.SetActiveHandler(func(e dwl_ipc.ZdwlIpcOutputV2ActiveEvent) {
state.active = e.Active != 0
})
dwlOut.SetXHandler(func(e dwl_ipc.ZdwlIpcOutputV2XEvent) {
state.x = e.X
})
dwlOut.SetYHandler(func(e dwl_ipc.ZdwlIpcOutputV2YEvent) {
state.y = e.Y
})
dwlOut.SetWidthHandler(func(e dwl_ipc.ZdwlIpcOutputV2WidthEvent) {
state.w = e.Width
})
dwlOut.SetHeightHandler(func(e dwl_ipc.ZdwlIpcOutputV2HeightEvent) {
state.h = e.Height
})
dwlOut.SetScalefactorHandler(func(e dwl_ipc.ZdwlIpcOutputV2ScalefactorEvent) {
state.scalefactor = e.Scalefactor
})
dwlOut.SetFrameHandler(func(e dwl_ipc.ZdwlIpcOutputV2FrameEvent) {
state.gotFrame = true
})
}
allFramesReceived := func() bool {
for _, s := range dwlOutputs {
if !s.gotFrame {
return false
}
}
return true
}
for !allFramesReceived() {
if err := ctx.Dispatch(); err != nil {
return nil, fmt.Errorf("dispatch: %w", err)
}
}
for _, state := range dwlOutputs {
if !state.active {
continue
}
if state.w <= 0 || state.h <= 0 {
return nil, fmt.Errorf("no active window")
}
scale := float64(state.scalefactor) / 100.0
if scale <= 0 {
scale = 1.0
}
geom := &WindowGeometry{
X: state.x,
Y: state.y,
Width: state.w,
Height: state.h,
Output: state.name,
Scale: scale,
}
if info, ok := getOutputInfo(state.name); ok {
geom.OutputX = info.x
geom.OutputY = info.y
geom.OutputTransform = info.transform
}
return geom, nil
}
return nil, fmt.Errorf("no active output found")
}

View File

@@ -20,7 +20,13 @@ func BufferToImageWithFormat(buf *ShmBuffer, format uint32) *image.RGBA {
img := image.NewRGBA(image.Rect(0, 0, buf.Width, buf.Height)) img := image.NewRGBA(image.Rect(0, 0, buf.Width, buf.Height))
data := buf.Data() data := buf.Data()
swapRB := format == uint32(FormatARGB8888) || format == uint32(FormatXRGB8888) || format == 0 var swapRB bool
switch format {
case uint32(FormatABGR8888), uint32(FormatXBGR8888):
swapRB = false
default:
swapRB = true
}
for y := 0; y < buf.Height; y++ { for y := 0; y < buf.Height; y++ {
srcOff := y * buf.Stride srcOff := y * buf.Stride

View File

@@ -380,19 +380,21 @@ func (r *RegionSelector) preCaptureOutput(output *WaylandOutput, pc *PreCapture,
return return
} }
var capturedBuf *ShmBuffer
frame.SetBufferHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1BufferEvent) { frame.SetBufferHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1BufferEvent) {
if int(e.Stride) < int(e.Width)*4 {
log.Error("invalid stride from compositor", "stride", e.Stride, "width", e.Width)
return
}
buf, err := CreateShmBuffer(int(e.Width), int(e.Height), int(e.Stride)) buf, err := CreateShmBuffer(int(e.Width), int(e.Height), int(e.Stride))
if err != nil { if err != nil {
log.Error("create screen buffer failed", "err", err) log.Error("create screen buffer failed", "err", err)
return return
} }
if withCursor { capturedBuf = buf
pc.screenBuf = buf
pc.format = e.Format pc.format = e.Format
} else {
pc.screenBufNoCursor = buf
}
pool, err := r.shm.CreatePool(buf.Fd(), int32(buf.Size())) pool, err := r.shm.CreatePool(buf.Fd(), int32(buf.Size()))
if err != nil { if err != nil {
@@ -421,6 +423,34 @@ func (r *RegionSelector) preCaptureOutput(output *WaylandOutput, pc *PreCapture,
frame.SetReadyHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1ReadyEvent) { frame.SetReadyHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1ReadyEvent) {
frame.Destroy() frame.Destroy()
if capturedBuf == nil {
onReady()
return
}
if pc.yInverted {
capturedBuf.FlipVertical()
pc.yInverted = false
}
if output.transform != TransformNormal {
invTransform := InverseTransform(output.transform)
transformed, err := capturedBuf.ApplyTransform(invTransform)
if err != nil {
log.Error("apply transform failed", "err", err)
} else if transformed != capturedBuf {
capturedBuf.Close()
capturedBuf = transformed
}
}
if withCursor {
pc.screenBuf = capturedBuf
} else {
pc.screenBufNoCursor = capturedBuf
}
onReady() onReady()
}) })

View File

@@ -135,16 +135,120 @@ func (s *Screenshoter) captureWindow() (*CaptureResult, error) {
Height: geom.Height, Height: geom.Height,
} }
output := s.findOutputForRegion(region) var output *WaylandOutput
if geom.Output != "" {
output = s.findOutputByName(geom.Output)
}
if output == nil {
output = s.findOutputForRegion(region)
}
if output == nil { if output == nil {
return nil, fmt.Errorf("could not find output for window") return nil, fmt.Errorf("could not find output for window")
} }
if DetectCompositor() == CompositorHyprland { switch DetectCompositor() {
case CompositorHyprland:
return s.captureAndCrop(output, region) return s.captureAndCrop(output, region)
case CompositorDWL:
return s.captureDWLWindow(output, region, geom)
default:
return s.captureRegionOnOutput(output, region)
}
}
func (s *Screenshoter) captureDWLWindow(output *WaylandOutput, region Region, geom *WindowGeometry) (*CaptureResult, error) {
result, err := s.captureWholeOutput(output)
if err != nil {
return nil, err
} }
return s.captureRegionOnOutput(output, region) scale := geom.Scale
if scale <= 0 || scale == 1.0 {
if output.fractionalScale > 1.0 {
scale = output.fractionalScale
}
}
if scale <= 0 {
scale = 1.0
}
localX := int(float64(region.X-geom.OutputX) * scale)
localY := int(float64(region.Y-geom.OutputY) * scale)
w := int(float64(region.Width) * scale)
h := int(float64(region.Height) * scale)
if localX < 0 {
w += localX
localX = 0
}
if localY < 0 {
h += localY
localY = 0
}
if localX+w > result.Buffer.Width {
w = result.Buffer.Width - localX
}
if localY+h > result.Buffer.Height {
h = result.Buffer.Height - localY
}
if w <= 0 || h <= 0 {
result.Buffer.Close()
return nil, fmt.Errorf("window not visible on output")
}
cropped, err := CreateShmBuffer(w, h, w*4)
if err != nil {
result.Buffer.Close()
return nil, fmt.Errorf("create crop buffer: %w", err)
}
srcData := result.Buffer.Data()
dstData := cropped.Data()
for y := 0; y < h; y++ {
srcY := localY + y
if result.YInverted {
srcY = result.Buffer.Height - 1 - (localY + y)
}
if srcY < 0 || srcY >= result.Buffer.Height {
continue
}
dstY := y
if result.YInverted {
dstY = h - 1 - y
}
for x := 0; x < w; x++ {
srcX := localX + x
if srcX < 0 || srcX >= result.Buffer.Width {
continue
}
si := srcY*result.Buffer.Stride + srcX*4
di := dstY*cropped.Stride + x*4
if si+3 >= len(srcData) || di+3 >= len(dstData) {
continue
}
dstData[di+0] = srcData[si+0]
dstData[di+1] = srcData[si+1]
dstData[di+2] = srcData[si+2]
dstData[di+3] = srcData[si+3]
}
}
result.Buffer.Close()
cropped.Format = PixelFormat(result.Format)
return &CaptureResult{
Buffer: cropped,
Region: region,
YInverted: false,
Format: result.Format,
}, nil
} }
func (s *Screenshoter) captureFullScreen() (*CaptureResult, error) { func (s *Screenshoter) captureFullScreen() (*CaptureResult, error) {
@@ -220,13 +324,18 @@ func (s *Screenshoter) captureAllScreens() (*CaptureResult, error) {
outX, outY := output.x, output.y outX, outY := output.x, output.y
scale := float64(output.scale) scale := float64(output.scale)
if DetectCompositor() == CompositorHyprland { switch DetectCompositor() {
case CompositorHyprland:
if hx, hy, _, _, ok := GetHyprlandMonitorGeometry(output.name); ok { if hx, hy, _, _, ok := GetHyprlandMonitorGeometry(output.name); ok {
outX, outY = hx, hy outX, outY = hx, hy
} }
if s := GetHyprlandMonitorScale(output.name); s > 0 { if s := GetHyprlandMonitorScale(output.name); s > 0 {
scale = s scale = s
} }
case CompositorDWL:
if info, ok := getOutputInfo(output.name); ok {
outX, outY = info.x, info.y
}
} }
if scale <= 0 { if scale <= 0 {
scale = 1.0 scale = 1.0
@@ -354,13 +463,42 @@ func (s *Screenshoter) captureWholeOutput(output *WaylandOutput) (*CaptureResult
return nil, fmt.Errorf("capture output: %w", err) return nil, fmt.Errorf("capture output: %w", err)
} }
return s.processFrame(frame, Region{ result, err := s.processFrame(frame, Region{
X: output.x, X: output.x,
Y: output.y, Y: output.y,
Width: output.width, Width: output.width,
Height: output.height, Height: output.height,
Output: output.name, Output: output.name,
}) })
if err != nil {
return nil, err
}
if result.YInverted {
result.Buffer.FlipVertical()
result.YInverted = false
}
if output.transform == TransformNormal {
return result, nil
}
invTransform := InverseTransform(output.transform)
transformed, err := result.Buffer.ApplyTransform(invTransform)
if err != nil {
result.Buffer.Close()
return nil, fmt.Errorf("apply transform: %w", err)
}
if transformed != result.Buffer {
result.Buffer.Close()
result.Buffer = transformed
}
result.Region.Width = int32(transformed.Width)
result.Region.Height = int32(transformed.Height)
return result, nil
} }
func (s *Screenshoter) captureAndCrop(output *WaylandOutput, region Region) (*CaptureResult, error) { func (s *Screenshoter) captureAndCrop(output *WaylandOutput, region Region) (*CaptureResult, error) {
@@ -441,6 +579,10 @@ func (s *Screenshoter) captureAndCrop(output *WaylandOutput, region Region) (*Ca
} }
func (s *Screenshoter) captureRegionOnOutput(output *WaylandOutput, region Region) (*CaptureResult, error) { func (s *Screenshoter) captureRegionOnOutput(output *WaylandOutput, region Region) (*CaptureResult, error) {
if output.transform != TransformNormal {
return s.captureRegionOnTransformedOutput(output, region)
}
scale := output.fractionalScale scale := output.fractionalScale
if scale <= 0 && DetectCompositor() == CompositorHyprland { if scale <= 0 && DetectCompositor() == CompositorHyprland {
scale = GetHyprlandMonitorScale(output.name) scale = GetHyprlandMonitorScale(output.name)
@@ -457,6 +599,31 @@ func (s *Screenshoter) captureRegionOnOutput(output *WaylandOutput, region Regio
w := int32(float64(region.Width) * scale) w := int32(float64(region.Width) * scale)
h := int32(float64(region.Height) * scale) h := int32(float64(region.Height) * scale)
if DetectCompositor() == CompositorDWL {
scaledOutW := int32(float64(output.width) * scale)
scaledOutH := int32(float64(output.height) * scale)
if localX >= scaledOutW {
localX = localX % scaledOutW
}
if localY >= scaledOutH {
localY = localY % scaledOutH
}
if localX+w > scaledOutW {
w = scaledOutW - localX
}
if localY+h > scaledOutH {
h = scaledOutH - localY
}
if localX < 0 {
w += localX
localX = 0
}
if localY < 0 {
h += localY
localY = 0
}
}
cursor := int32(0) cursor := int32(0)
if s.config.IncludeCursor { if s.config.IncludeCursor {
cursor = 1 cursor = 1
@@ -470,6 +637,76 @@ func (s *Screenshoter) captureRegionOnOutput(output *WaylandOutput, region Regio
return s.processFrame(frame, region) return s.processFrame(frame, region)
} }
func (s *Screenshoter) captureRegionOnTransformedOutput(output *WaylandOutput, region Region) (*CaptureResult, error) {
result, err := s.captureWholeOutput(output)
if err != nil {
return nil, err
}
scale := output.fractionalScale
if scale <= 0 && DetectCompositor() == CompositorHyprland {
scale = GetHyprlandMonitorScale(output.name)
}
if scale <= 0 {
scale = float64(output.scale)
}
if scale <= 0 {
scale = 1.0
}
localX := int(float64(region.X-output.x) * scale)
localY := int(float64(region.Y-output.y) * scale)
w := int(float64(region.Width) * scale)
h := int(float64(region.Height) * scale)
if localX < 0 {
w += localX
localX = 0
}
if localY < 0 {
h += localY
localY = 0
}
if localX+w > result.Buffer.Width {
w = result.Buffer.Width - localX
}
if localY+h > result.Buffer.Height {
h = result.Buffer.Height - localY
}
if w <= 0 || h <= 0 {
result.Buffer.Close()
return nil, fmt.Errorf("region not visible on output")
}
cropped, err := CreateShmBuffer(w, h, w*4)
if err != nil {
result.Buffer.Close()
return nil, fmt.Errorf("create crop buffer: %w", err)
}
srcData := result.Buffer.Data()
dstData := cropped.Data()
for y := 0; y < h; y++ {
srcOff := (localY+y)*result.Buffer.Stride + localX*4
dstOff := y * cropped.Stride
if srcOff+w*4 <= len(srcData) && dstOff+w*4 <= len(dstData) {
copy(dstData[dstOff:dstOff+w*4], srcData[srcOff:srcOff+w*4])
}
}
result.Buffer.Close()
cropped.Format = PixelFormat(result.Format)
return &CaptureResult{
Buffer: cropped,
Region: region,
YInverted: false,
Format: result.Format,
}, nil
}
func (s *Screenshoter) processFrame(frame *wlr_screencopy.ZwlrScreencopyFrameV1, region Region) (*CaptureResult, error) { func (s *Screenshoter) processFrame(frame *wlr_screencopy.ZwlrScreencopyFrameV1, region Region) (*CaptureResult, error) {
var buf *ShmBuffer var buf *ShmBuffer
var pool *client.ShmPool var pool *client.ShmPool
@@ -480,6 +717,10 @@ func (s *Screenshoter) processFrame(frame *wlr_screencopy.ZwlrScreencopyFrameV1,
failed := false failed := false
frame.SetBufferHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1BufferEvent) { frame.SetBufferHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1BufferEvent) {
if int(e.Stride) < int(e.Width)*4 {
log.Error("invalid stride from compositor", "stride", e.Stride, "width", e.Width)
return
}
var err error var err error
buf, err = CreateShmBuffer(int(e.Width), int(e.Height), int(e.Stride)) buf, err = CreateShmBuffer(int(e.Width), int(e.Height), int(e.Stride))
if err != nil { if err != nil {
@@ -557,6 +798,17 @@ func (s *Screenshoter) processFrame(frame *wlr_screencopy.ZwlrScreencopyFrameV1,
}, nil }, nil
} }
func (s *Screenshoter) findOutputByName(name string) *WaylandOutput {
s.outputsMu.Lock()
defer s.outputsMu.Unlock()
for _, o := range s.outputs {
if o.name == name {
return o
}
}
return nil
}
func (s *Screenshoter) findOutputForRegion(region Region) *WaylandOutput { func (s *Screenshoter) findOutputForRegion(region Region) *WaylandOutput {
s.outputsMu.Lock() s.outputsMu.Lock()
defer s.outputsMu.Unlock() defer s.outputsMu.Unlock()
@@ -766,16 +1018,32 @@ func ListOutputs() ([]Output, error) {
sc.outputsMu.Lock() sc.outputsMu.Lock()
defer sc.outputsMu.Unlock() defer sc.outputsMu.Unlock()
compositor := DetectCompositor()
result := make([]Output, 0, len(sc.outputs)) result := make([]Output, 0, len(sc.outputs))
for _, o := range sc.outputs { for _, o := range sc.outputs {
result = append(result, Output{ out := Output{
Name: o.name, Name: o.name,
X: o.x, X: o.x,
Y: o.y, Y: o.y,
Width: o.width, Width: o.width,
Height: o.height, Height: o.height,
Scale: o.scale, Scale: o.scale,
}) FractionalScale: o.fractionalScale,
Transform: o.transform,
}
switch compositor {
case CompositorHyprland:
if hx, hy, hw, hh, ok := GetHyprlandMonitorGeometry(o.name); ok {
out.X, out.Y = hx, hy
out.Width, out.Height = hw, hh
}
if s := GetHyprlandMonitorScale(o.name); s > 0 {
out.FractionalScale = s
}
}
result = append(result, out)
} }
return result, nil return result, nil
} }

View File

@@ -11,8 +11,23 @@ const (
FormatXBGR8888 = shm.FormatXBGR8888 FormatXBGR8888 = shm.FormatXBGR8888
) )
const (
TransformNormal = shm.TransformNormal
Transform90 = shm.Transform90
Transform180 = shm.Transform180
Transform270 = shm.Transform270
TransformFlipped = shm.TransformFlipped
TransformFlipped90 = shm.TransformFlipped90
TransformFlipped180 = shm.TransformFlipped180
TransformFlipped270 = shm.TransformFlipped270
)
type ShmBuffer = shm.Buffer type ShmBuffer = shm.Buffer
func CreateShmBuffer(width, height, stride int) (*ShmBuffer, error) { func CreateShmBuffer(width, height, stride int) (*ShmBuffer, error) {
return shm.CreateBuffer(width, height, stride) return shm.CreateBuffer(width, height, stride)
} }
func InverseTransform(transform int32) int32 {
return shm.InverseTransform(transform)
}

View File

@@ -37,6 +37,8 @@ type Output struct {
Width int32 Width int32
Height int32 Height int32
Scale int32 Scale int32
FractionalScale float64
Transform int32
} }
type Config struct { type Config struct {

View File

@@ -306,6 +306,15 @@ func (m *Manager) readAndUpdateCapsLockState(deviceIndex int) {
return return
} }
if len(ledStates) == 0 {
log.Debug("No LED state available (empty map)")
// This means the device either:
// - doesn't support LED reporting at all, or
// - the kernel returned an empty state
return
}
capsLockState := ledStates[ledCapslockKey] capsLockState := ledStates[ledCapslockKey]
m.updateCapsLockStateDirect(capsLockState) m.updateCapsLockStateDirect(capsLockState)
} }

View File

@@ -137,3 +137,96 @@ func (b *Buffer) Clear() {
func (b *Buffer) CopyFrom(src *Buffer) { func (b *Buffer) CopyFrom(src *Buffer) {
copy(b.data, src.data) copy(b.data, src.data)
} }
const (
TransformNormal = 0
Transform90 = 1
Transform180 = 2
Transform270 = 3
TransformFlipped = 4
TransformFlipped90 = 5
TransformFlipped180 = 6
TransformFlipped270 = 7
)
func (b *Buffer) ApplyTransform(transform int32) (*Buffer, error) {
if transform == TransformNormal {
return b, nil
}
var newW, newH int
switch transform {
case Transform90, Transform270, TransformFlipped90, TransformFlipped270:
newW, newH = b.Height, b.Width
default:
newW, newH = b.Width, b.Height
}
newBuf, err := CreateBuffer(newW, newH, newW*4)
if err != nil {
return nil, err
}
newBuf.Format = b.Format
srcData := b.data
dstData := newBuf.data
for sy := 0; sy < b.Height; sy++ {
for sx := 0; sx < b.Width; sx++ {
var dx, dy int
switch transform {
case Transform90: // 90° CCW
dx = sy
dy = b.Width - 1 - sx
case Transform180:
dx = b.Width - 1 - sx
dy = b.Height - 1 - sy
case Transform270: // 270° CCW = 90° CW
dx = b.Height - 1 - sy
dy = sx
case TransformFlipped:
dx = b.Width - 1 - sx
dy = sy
case TransformFlipped90:
dx = sy
dy = sx
case TransformFlipped180:
dx = sx
dy = b.Height - 1 - sy
case TransformFlipped270:
dx = b.Height - 1 - sy
dy = b.Width - 1 - sx
default:
dx, dy = sx, sy
}
si := sy*b.Stride + sx*4
di := dy*newBuf.Stride + dx*4
if si+3 < len(srcData) && di+3 < len(dstData) {
dstData[di+0] = srcData[si+0]
dstData[di+1] = srcData[si+1]
dstData[di+2] = srcData[si+2]
dstData[di+3] = srcData[si+3]
}
}
}
return newBuf, nil
}
func InverseTransform(transform int32) int32 {
switch transform {
case Transform90:
return Transform270
case Transform270:
return Transform90
case TransformFlipped90:
return TransformFlipped270
case TransformFlipped270:
return TransformFlipped90
default:
return transform
}
}

View File

@@ -71,7 +71,7 @@
nativeBuildInputs = with pkgs; [ nativeBuildInputs = with pkgs; [
installShellFiles installShellFiles
.makeWrapper makeWrapper
]; ];
postInstall = '' postInstall = ''

View File

@@ -295,7 +295,7 @@ Singleton {
property bool lockScreenShowPowerActions: true property bool lockScreenShowPowerActions: true
property bool enableFprint: false property bool enableFprint: false
property int maxFprintTries: 3 property int maxFprintTries: 15
property bool fprintdAvailable: false property bool fprintdAvailable: false
property string lockScreenActiveMonitor: "all" property string lockScreenActiveMonitor: "all"
property string lockScreenInactiveColor: "#000000" property string lockScreenInactiveColor: "#000000"

View File

@@ -194,7 +194,7 @@ var SPEC = {
lockScreenShowPowerActions: { def: true }, lockScreenShowPowerActions: { def: true },
enableFprint: { def: false }, enableFprint: { def: false },
maxFprintTries: { def: 3 }, maxFprintTries: { def: 15 },
fprintdAvailable: { def: false, persist: false }, fprintdAvailable: { def: false, persist: false },
lockScreenActiveMonitor: { def: "all" }, lockScreenActiveMonitor: { def: "all" },
lockScreenInactiveColor: { def: "#000000" }, lockScreenInactiveColor: { def: "#000000" },

View File

@@ -108,9 +108,10 @@ Item {
id: barRepeaterModel id: barRepeaterModel
values: { values: {
const configs = SettingsData.barConfigs; const configs = SettingsData.barConfigs;
return configs return configs.map(c => ({
.map(c => ({ id: c.id, position: c.position })) id: c.id,
.sort((a, b) => { position: c.position
})).sort((a, b) => {
const aVertical = a.position === SettingsData.Position.Left || a.position === SettingsData.Position.Right; const aVertical = a.position === SettingsData.Position.Left || a.position === SettingsData.Position.Right;
const bVertical = b.position === SettingsData.Position.Left || b.position === SettingsData.Position.Right; const bVertical = b.position === SettingsData.Position.Left || b.position === SettingsData.Position.Right;
return aVertical - bVertical; return aVertical - bVertical;
@@ -142,9 +143,34 @@ Item {
} }
} }
property bool dockEnabled: false
Timer {
id: dockRecreateDebounce
interval: 500
repeat: false
onTriggered: {
root.dockEnabled = false;
Qt.callLater(() => {
root.dockEnabled = true;
});
}
}
Component.onCompleted: {
dockRecreateDebounce.start();
}
Connections {
target: SettingsData
function onBarConfigsChanged() {
dockRecreateDebounce.restart();
}
}
Loader { Loader {
id: dockLoader id: dockLoader
active: true active: root.dockEnabled
asynchronous: false asynchronous: false
property var currentPosition: SettingsData.dockPosition property var currentPosition: SettingsData.dockPosition
@@ -440,62 +466,71 @@ Item {
title: I18n.tr("Open with...") title: I18n.tr("Open with...")
function shellEscape(str) { function shellEscape(str) {
return "'" + str.replace(/'/g, "'\\''") + "'" return "'" + str.replace(/'/g, "'\\''") + "'";
} }
onApplicationSelected: (app, filePath) => { onApplicationSelected: (app, filePath) => {
if (!app) return if (!app)
return;
let cmd = app.exec || "";
const escapedPath = shellEscape(filePath);
const escapedUri = shellEscape("file://" + filePath);
let cmd = app.exec || "" let hasField = false;
const escapedPath = shellEscape(filePath) if (cmd.includes("%f")) {
const escapedUri = shellEscape("file://" + filePath) cmd = cmd.replace("%f", escapedPath);
hasField = true;
let hasField = false } else if (cmd.includes("%F")) {
if (cmd.includes("%f")) { cmd = cmd.replace("%f", escapedPath); hasField = true } cmd = cmd.replace("%F", escapedPath);
else if (cmd.includes("%F")) { cmd = cmd.replace("%F", escapedPath); hasField = true } hasField = true;
else if (cmd.includes("%u")) { cmd = cmd.replace("%u", escapedUri); hasField = true } } else if (cmd.includes("%u")) {
else if (cmd.includes("%U")) { cmd = cmd.replace("%U", escapedUri); hasField = true } cmd = cmd.replace("%u", escapedUri);
hasField = true;
cmd = cmd.replace(/%[ikc]/g, "") } else if (cmd.includes("%U")) {
cmd = cmd.replace("%U", escapedUri);
if (!hasField) { hasField = true;
cmd += " " + escapedPath
} }
console.log("FilePicker: Launching", cmd) cmd = cmd.replace(/%[ikc]/g, "");
if (!hasField) {
cmd += " " + escapedPath;
}
console.log("FilePicker: Launching", cmd);
Quickshell.execDetached({ Quickshell.execDetached({
command: ["sh", "-c", cmd] command: ["sh", "-c", cmd]
}) });
} }
} }
Connections { Connections {
target: DMSService target: DMSService
function onOpenUrlRequested(url) { function onOpenUrlRequested(url) {
browserPickerModal.url = url browserPickerModal.url = url;
browserPickerModal.open() browserPickerModal.open();
} }
function onAppPickerRequested(data) { function onAppPickerRequested(data) {
console.log("DMSShell: App picker requested with data:", JSON.stringify(data)) console.log("DMSShell: App picker requested with data:", JSON.stringify(data));
if (!data || !data.target) { if (!data || !data.target) {
console.warn("DMSShell: Invalid app picker request data") console.warn("DMSShell: Invalid app picker request data");
return return;
} }
filePickerModal.targetData = data.target filePickerModal.targetData = data.target;
filePickerModal.targetDataLabel = data.requestType || "file" filePickerModal.targetDataLabel = data.requestType || "file";
if (data.categories && data.categories.length > 0) { if (data.categories && data.categories.length > 0) {
filePickerModal.categoryFilter = data.categories filePickerModal.categoryFilter = data.categories;
} else { } else {
filePickerModal.categoryFilter = [] filePickerModal.categoryFilter = [];
} }
filePickerModal.usageHistoryKey = "filePickerUsageHistory" filePickerModal.usageHistoryKey = "filePickerUsageHistory";
filePickerModal.open() filePickerModal.open();
} }
} }

View File

@@ -46,6 +46,7 @@ FocusScope {
property bool pathInputHasFocus: false property bool pathInputHasFocus: false
property int actualGridColumns: 5 property int actualGridColumns: 5
property bool _initialized: false property bool _initialized: false
property bool closeOnEscape: true
signal fileSelected(string path) signal fileSelected(string path)
signal closeRequested signal closeRequested
@@ -298,7 +299,7 @@ FocusScope {
property int gridColumns: viewMode === "list" ? 1 : Math.max(1, actualGridColumns) property int gridColumns: viewMode === "list" ? 1 : Math.max(1, actualGridColumns)
function handleKey(event) { function handleKey(event) {
if (event.key === Qt.Key_Escape) { if (event.key === Qt.Key_Escape && root.closeOnEscape) {
closeRequested(); closeRequested();
event.accepted = true; event.accepted = true;
return; return;

View File

@@ -35,7 +35,7 @@ FloatingWindow {
minimumSize: Qt.size(500, 400) minimumSize: Qt.size(500, 400)
implicitWidth: 800 implicitWidth: 800
implicitHeight: 600 implicitHeight: 600
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) color: Theme.surfaceContainer
visible: false visible: false
onVisibleChanged: { onVisibleChanged: {
@@ -59,6 +59,7 @@ FloatingWindow {
id: content id: content
anchors.fill: parent anchors.fill: parent
focus: true focus: true
closeOnEscape: false
browserTitle: fileBrowserModal.browserTitle browserTitle: fileBrowserModal.browserTitle
browserIcon: fileBrowserModal.browserIcon browserIcon: fileBrowserModal.browserIcon

View File

@@ -59,6 +59,7 @@ DankModal {
modalWidth: 500 modalWidth: 500
modalHeight: 700 modalHeight: 700
backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
visible: false visible: false
onBackgroundClicked: hide() onBackgroundClicked: hide()
onOpened: () => { onOpened: () => {

View File

@@ -45,7 +45,7 @@ FloatingWindow {
title: I18n.tr("Authentication") title: I18n.tr("Authentication")
minimumSize: Qt.size(420, calculatedHeight) minimumSize: Qt.size(420, calculatedHeight)
maximumSize: Qt.size(420, calculatedHeight) maximumSize: Qt.size(420, calculatedHeight)
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) color: Theme.surfaceContainer
visible: false visible: false
onVisibleChanged: { onVisibleChanged: {

View File

@@ -66,7 +66,7 @@ FloatingWindow {
minimumSize: Qt.size(650, 400) minimumSize: Qt.size(650, 400)
implicitWidth: 900 implicitWidth: 900
implicitHeight: 680 implicitHeight: 680
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) color: Theme.surfaceContainer
visible: false visible: false
onVisibleChanged: { onVisibleChanged: {
@@ -112,12 +112,6 @@ FloatingWindow {
focus: true focus: true
Keys.onPressed: event => { Keys.onPressed: event => {
if (event.key === Qt.Key_Escape) {
hide();
event.accepted = true;
return;
}
switch (event.key) { switch (event.key) {
case Qt.Key_1: case Qt.Key_1:
currentTab = 0; currentTab = 0;

View File

@@ -60,7 +60,7 @@ FloatingWindow {
minimumSize: Qt.size(500, 400) minimumSize: Qt.size(500, 400)
implicitWidth: 800 implicitWidth: 800
implicitHeight: 940 implicitHeight: 940
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) color: Theme.surfaceContainer
visible: false visible: false
onIsCompactModeChanged: { onIsCompactModeChanged: {
@@ -139,11 +139,6 @@ FloatingWindow {
focus: true focus: true
Keys.onPressed: event => { Keys.onPressed: event => {
if (event.key === Qt.Key_Escape) {
hide();
event.accepted = true;
return;
}
if (event.key === Qt.Key_Down || (event.key === Qt.Key_Tab && !event.modifiers)) { if (event.key === Qt.Key_Down || (event.key === Qt.Key_Tab && !event.modifiers)) {
sidebar.navigateNext(); sidebar.navigateNext();
event.accepted = true; event.accepted = true;

View File

@@ -191,7 +191,7 @@ FloatingWindow {
title: isVpnPrompt ? I18n.tr("VPN Password") : I18n.tr("Wi-Fi Password") title: isVpnPrompt ? I18n.tr("VPN Password") : I18n.tr("Wi-Fi Password")
minimumSize: Qt.size(420, calculatedHeight) minimumSize: Qt.size(420, calculatedHeight)
maximumSize: Qt.size(420, calculatedHeight) maximumSize: Qt.size(420, calculatedHeight)
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) color: Theme.surfaceContainer
visible: false visible: false
onVisibleChanged: { onVisibleChanged: {

View File

@@ -97,7 +97,7 @@ DankPopout {
property alias searchField: searchField property alias searchField: searchField
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) color: "transparent"
radius: Theme.cornerRadius radius: Theme.cornerRadius
antialiasing: true antialiasing: true
smooth: true smooth: true

View File

@@ -113,11 +113,7 @@ DankPopout {
implicitHeight: mainColumn.implicitHeight + Theme.spacingM implicitHeight: mainColumn.implicitHeight + Theme.spacingM
property alias bluetoothCodecSelector: bluetoothCodecSelector property alias bluetoothCodecSelector: bluetoothCodecSelector
color: { color: "transparent"
const transparency = Theme.popupTransparency;
const surface = Theme.surfaceContainer || Qt.rgba(0.1, 0.1, 0.1, 1);
return Qt.rgba(surface.r, surface.g, surface.b, transparency);
}
radius: Theme.cornerRadius radius: Theme.cornerRadius
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 0 border.width: 0

View File

@@ -56,6 +56,8 @@ Row {
} }
DankSlider { DankSlider {
id: volumeSlider
readonly property real actualVolumePercent: defaultSink ? Math.round(defaultSink.audio.volume * 100) : 0 readonly property real actualVolumePercent: defaultSink ? Math.round(defaultSink.audio.volume * 100) : 0
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
@@ -63,7 +65,6 @@ Row {
enabled: defaultSink !== null enabled: defaultSink !== null
minimum: 0 minimum: 0
maximum: 100 maximum: 100
value: defaultSink ? Math.min(100, Math.round(defaultSink.audio.volume * 100)) : 0
showValue: true showValue: true
unit: "%" unit: "%"
valueOverride: actualVolumePercent valueOverride: actualVolumePercent
@@ -81,4 +82,11 @@ Row {
} }
} }
} }
Binding {
target: volumeSlider
property: "value"
value: defaultSink ? Math.min(100, Math.round(defaultSink.audio.volume * 100)) : 0
when: !volumeSlider.isDragging
}
} }

View File

@@ -1,9 +1,6 @@
import QtQuick import QtQuick
import QtQuick.Controls
import Quickshell import Quickshell
import Quickshell.Services.UPower import Quickshell.Services.UPower
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common import qs.Common
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
@@ -26,13 +23,12 @@ DankPopout {
function setProfile(profile) { function setProfile(profile) {
if (typeof PowerProfiles === "undefined") { if (typeof PowerProfiles === "undefined") {
ToastService.showError("power-profiles-daemon not available"); ToastService.showError("power-profiles-daemon not available");
return ; return;
} }
PowerProfiles.profile = profile; PowerProfiles.profile = profile;
if (PowerProfiles.profile !== profile) { if (PowerProfiles.profile !== profile) {
ToastService.showError("Failed to set power profile"); ToastService.showError("Failed to set power profile");
} }
} }
popupWidth: 400 popupWidth: 400
@@ -48,7 +44,7 @@ DankPopout {
id: batteryContent id: batteryContent
implicitHeight: contentColumn.implicitHeight + Theme.spacingL * 2 implicitHeight: contentColumn.implicitHeight + Theme.spacingL * 2
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) color: "transparent"
radius: Theme.cornerRadius radius: Theme.cornerRadius
border.color: Theme.outlineMedium border.color: Theme.outlineMedium
border.width: 0 border.width: 0
@@ -59,9 +55,8 @@ DankPopout {
if (root.shouldBeVisible) { if (root.shouldBeVisible) {
forceActiveFocus(); forceActiveFocus();
} }
} }
Keys.onPressed: function(event) { Keys.onPressed: function (event) {
if (event.key === Qt.Key_Escape) { if (event.key === Qt.Key_Escape) {
root.close(); root.close();
event.accepted = true; event.accepted = true;
@@ -71,11 +66,10 @@ DankPopout {
Connections { Connections {
function onShouldBeVisibleChanged() { function onShouldBeVisibleChanged() {
if (root.shouldBeVisible) { if (root.shouldBeVisible) {
Qt.callLater(function() { Qt.callLater(function () {
batteryContent.forceActiveFocus(); batteryContent.forceActiveFocus();
}); });
} }
} }
target: root target: root
@@ -246,7 +240,8 @@ DankPopout {
StyledText { StyledText {
text: { text: {
if (!BatteryService.batteryAvailable) return "Power profile management available" if (!BatteryService.batteryAvailable)
return "Power profile management available";
const time = BatteryService.formatTimeRemaining(); const time = BatteryService.formatTimeRemaining();
if (time !== "Unknown") { if (time !== "Unknown") {
return BatteryService.isCharging ? `Time until full: ${time}` : `Time remaining: ${time}`; return BatteryService.isCharging ? `Time until full: ${time}` : `Time remaining: ${time}`;
@@ -470,14 +465,14 @@ DankPopout {
StyledText { StyledText {
text: { text: {
if (!modelData.healthSupported || modelData.healthPercentage <= 0) if (!modelData.healthSupported || modelData.healthPercentage <= 0)
return "N/A" return "N/A";
return `${Math.round(modelData.healthPercentage)}%` return `${Math.round(modelData.healthPercentage)}%`;
} }
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: { color: {
if (!modelData.healthSupported || modelData.healthPercentage <= 0) if (!modelData.healthSupported || modelData.healthPercentage <= 0)
return Theme.surfaceText return Theme.surfaceText;
return modelData.healthPercentage < 80 ? Theme.error : Theme.surfaceText return modelData.healthPercentage < 80 ? Theme.error : Theme.surfaceText;
} }
font.weight: Font.Bold font.weight: Font.Bold
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
@@ -526,10 +521,7 @@ DankPopout {
spacing: 2 spacing: 2
StyledText { StyledText {
text: modelData.state === UPowerDeviceState.Charging text: modelData.state === UPowerDeviceState.Charging ? I18n.tr("To Full") : modelData.state === UPowerDeviceState.Discharging ? I18n.tr("Left") : ""
? I18n.tr("To Full")
: modelData.state === UPowerDeviceState.Discharging
? I18n.tr("Left") : ""
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium color: Theme.surfaceTextMedium
font.weight: Font.Medium font.weight: Font.Medium
@@ -538,17 +530,14 @@ DankPopout {
StyledText { StyledText {
text: { text: {
const time = modelData.state === UPowerDeviceState.Charging const time = modelData.state === UPowerDeviceState.Charging ? modelData.timeToFull : modelData.state === UPowerDeviceState.Discharging && BatteryService.changeRate > 0 ? (3600 * modelData.energy) / BatteryService.changeRate : 0;
? modelData.timeToFull
: modelData.state === UPowerDeviceState.Discharging && BatteryService.changeRate > 0
? (3600 * modelData.energy) / BatteryService.changeRate : 0
if (!time || time <= 0 || time > 86400) if (!time || time <= 0 || time > 86400)
return "N/A" return "N/A";
const hours = Math.floor(time / 3600) const hours = Math.floor(time / 3600);
const minutes = Math.floor((time % 3600) / 60) const minutes = Math.floor((time % 3600) / 60);
return hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m` return hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`;
} }
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText color: Theme.surfaceText
@@ -566,8 +555,9 @@ DankPopout {
DankButtonGroup { DankButtonGroup {
property var profileModel: (typeof PowerProfiles !== "undefined") ? [PowerProfile.PowerSaver, PowerProfile.Balanced].concat(PowerProfiles.hasPerformanceProfile ? [PowerProfile.Performance] : []) : [PowerProfile.PowerSaver, PowerProfile.Balanced, PowerProfile.Performance] property var profileModel: (typeof PowerProfiles !== "undefined") ? [PowerProfile.PowerSaver, PowerProfile.Balanced].concat(PowerProfiles.hasPerformanceProfile ? [PowerProfile.Performance] : []) : [PowerProfile.PowerSaver, PowerProfile.Balanced, PowerProfile.Performance]
property int currentProfileIndex: { property int currentProfileIndex: {
if (typeof PowerProfiles === "undefined") return 1 if (typeof PowerProfiles === "undefined")
return profileModel.findIndex(profile => root.isActiveProfile(profile)) return 1;
return profileModel.findIndex(profile => root.isActiveProfile(profile));
} }
model: profileModel.map(profile => Theme.getPowerProfileLabel(profile)) model: profileModel.map(profile => Theme.getPowerProfileLabel(profile))
@@ -575,8 +565,9 @@ DankPopout {
selectionMode: "single" selectionMode: "single"
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
onSelectionChanged: (index, selected) => { onSelectionChanged: (index, selected) => {
if (!selected) return if (!selected)
root.setProfile(profileModel[index]) return;
root.setProfile(profileModel[index]);
} }
} }
@@ -634,5 +625,4 @@ DankPopout {
} }
} }
} }
} }

View File

@@ -1,8 +1,4 @@
import QtQuick import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common import qs.Common
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
@@ -15,31 +11,31 @@ DankPopout {
property var triggerScreen: null property var triggerScreen: null
function setTriggerPosition(x, y, width, section, screen, barPosition, barThickness, barSpacing, barConfig) { function setTriggerPosition(x, y, width, section, screen, barPosition, barThickness, barSpacing, barConfig) {
triggerX = x triggerX = x;
triggerY = y triggerY = y;
triggerWidth = width triggerWidth = width;
triggerSection = section triggerSection = section;
root.screen = screen root.screen = screen;
storedBarThickness = barThickness !== undefined ? barThickness : (Theme.barHeight - 4) storedBarThickness = barThickness !== undefined ? barThickness : (Theme.barHeight - 4);
storedBarSpacing = barSpacing !== undefined ? barSpacing : 4 storedBarSpacing = barSpacing !== undefined ? barSpacing : 4;
storedBarConfig = barConfig storedBarConfig = barConfig;
const pos = barPosition !== undefined ? barPosition : 0 const pos = barPosition !== undefined ? barPosition : 0;
const bottomGap = barConfig ? (barConfig.bottomGap !== undefined ? barConfig.bottomGap : 0) : 0 const bottomGap = barConfig ? (barConfig.bottomGap !== undefined ? barConfig.bottomGap : 0) : 0;
setBarContext(pos, bottomGap) setBarContext(pos, bottomGap);
updateOutputState() updateOutputState();
} }
onScreenChanged: updateOutputState() onScreenChanged: updateOutputState()
function updateOutputState() { function updateOutputState() {
if (screen && DwlService.dwlAvailable) { if (screen && DwlService.dwlAvailable) {
outputState = DwlService.getOutputState(screen.name) outputState = DwlService.getOutputState(screen.name);
} else { } else {
outputState = null outputState = null;
} }
} }
@@ -75,28 +71,28 @@ DankPopout {
}) })
function getLayoutName(symbol) { function getLayoutName(symbol) {
return layoutNames[symbol] || symbol return layoutNames[symbol] || symbol;
} }
function getLayoutIcon(symbol) { function getLayoutIcon(symbol) {
return layoutIcons[symbol] || "view_quilt" return layoutIcons[symbol] || "view_quilt";
} }
Connections { Connections {
target: DwlService target: DwlService
function onStateChanged() { function onStateChanged() {
updateOutputState() updateOutputState();
} }
} }
onShouldBeVisibleChanged: { onShouldBeVisibleChanged: {
if (shouldBeVisible) { if (shouldBeVisible) {
updateOutputState() updateOutputState();
} }
} }
Component.onCompleted: { Component.onCompleted: {
updateOutputState() updateOutputState();
} }
popupWidth: 300 popupWidth: 300
@@ -111,7 +107,7 @@ DankPopout {
id: layoutContent id: layoutContent
implicitHeight: contentColumn.implicitHeight + Theme.spacingL * 2 implicitHeight: contentColumn.implicitHeight + Theme.spacingL * 2
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) color: "transparent"
radius: Theme.cornerRadius radius: Theme.cornerRadius
border.color: Theme.outlineMedium border.color: Theme.outlineMedium
border.width: 0 border.width: 0
@@ -121,14 +117,14 @@ DankPopout {
Component.onCompleted: { Component.onCompleted: {
if (root.shouldBeVisible) { if (root.shouldBeVisible) {
forceActiveFocus() forceActiveFocus();
} }
} }
Keys.onPressed: event => { Keys.onPressed: event => {
if (event.key === Qt.Key_Escape) { if (event.key === Qt.Key_Escape) {
root.close() root.close();
event.accepted = true event.accepted = true;
} }
} }
@@ -137,8 +133,8 @@ DankPopout {
function onShouldBeVisibleChanged() { function onShouldBeVisibleChanged() {
if (root.shouldBeVisible) { if (root.shouldBeVisible) {
Qt.callLater(() => { Qt.callLater(() => {
layoutContent.forceActiveFocus() layoutContent.forceActiveFocus();
}) });
} }
} }
} }
@@ -212,7 +208,7 @@ DankPopout {
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onPressed: { onPressed: {
root.close() root.close();
} }
} }
} }
@@ -282,14 +278,14 @@ DankPopout {
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onPressed: { onPressed: {
if (!root.triggerScreen) { if (!root.triggerScreen) {
return return;
} }
if (!DwlService.dwlAvailable) { if (!DwlService.dwlAvailable) {
return return;
} }
DwlService.setLayout(root.triggerScreen.name, index) DwlService.setLayout(root.triggerScreen.name, index);
root.close() root.close();
} }
} }

View File

@@ -36,7 +36,7 @@ DankPopout {
id: content id: content
implicitHeight: contentColumn.height + Theme.spacingL * 2 implicitHeight: contentColumn.height + Theme.spacingL * 2
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) color: "transparent"
radius: Theme.cornerRadius radius: Theme.cornerRadius
border.color: Theme.outlineMedium border.color: Theme.outlineMedium
border.width: 0 border.width: 0

View File

@@ -167,7 +167,7 @@ DankPopout {
id: mainContainer id: mainContainer
implicitHeight: contentColumn.height + Theme.spacingM * 2 implicitHeight: contentColumn.height + Theme.spacingM * 2
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) color: "transparent"
radius: Theme.cornerRadius radius: Theme.cornerRadius
focus: true focus: true
@@ -358,6 +358,8 @@ DankPopout {
popoutWidth: root.alignedWidth popoutWidth: root.alignedWidth
popoutHeight: root.alignedHeight popoutHeight: root.alignedHeight
contentOffsetY: Theme.spacingM + 48 + Theme.spacingS + Theme.spacingXS contentOffsetY: Theme.spacingM + 48 + Theme.spacingS + Theme.spacingXS
section: root.triggerSection
barPosition: root.effectiveBarPosition
Component.onCompleted: root.__mediaTabRef = this Component.onCompleted: root.__mediaTabRef = this
onShowVolumeDropdown: (pos, screen, rightEdge, player, players) => { onShowVolumeDropdown: (pos, screen, rightEdge, player, players) => {
root.__showVolumeDropdown(pos, rightEdge, player, players); root.__showVolumeDropdown(pos, rightEdge, player, players);

View File

@@ -3,7 +3,6 @@ import QtQuick.Effects
import QtQuick.Layouts import QtQuick.Layouts
import Quickshell.Services.Mpris import Quickshell.Services.Mpris
import Quickshell.Io import Quickshell.Io
import Quickshell
import qs.Common import qs.Common
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
@@ -19,6 +18,8 @@ Item {
property real popoutWidth: 0 property real popoutWidth: 0
property real popoutHeight: 0 property real popoutHeight: 0
property real contentOffsetY: 0 property real contentOffsetY: 0
property string section: ""
property int barPosition: SettingsData.Position.Top
signal showVolumeDropdown(point pos, var screen, bool rightEdge, var player, var players) signal showVolumeDropdown(point pos, var screen, bool rightEdge, var player, var players)
signal showAudioDevicesDropdown(point pos, var screen, bool rightEdge) signal showAudioDevicesDropdown(point pos, var screen, bool rightEdge)
@@ -40,7 +41,13 @@ Item {
id: sharedTooltip id: sharedTooltip
} }
readonly property bool isRightEdge: (SettingsData.barConfigs[0]?.position ?? SettingsData.Position.Top) === SettingsData.Position.Right readonly property bool isRightEdge: {
if (barPosition === SettingsData.Position.Right)
return true;
if (barPosition === SettingsData.Position.Left)
return false;
return section === "right";
}
readonly property bool __isChromeBrowser: { readonly property bool __isChromeBrowser: {
if (!activePlayer?.identity) if (!activePlayer?.identity)
return false; return false;

View File

@@ -4,6 +4,7 @@ import QtQuick.Controls
import QtQuick.Effects import QtQuick.Effects
import QtQuick.Layouts import QtQuick.Layouts
import Quickshell import Quickshell
import Quickshell.Hyprland
import Quickshell.Io import Quickshell.Io
import Quickshell.Services.Greetd import Quickshell.Services.Greetd
import Quickshell.Services.Pam import Quickshell.Services.Pam
@@ -35,19 +36,40 @@ Item {
randomFact = Facts.getRandomFact() randomFact = Facts.getRandomFact()
} }
property bool weatherInitialized: false
function initWeatherService() {
if (weatherInitialized)
return
if (!GreetdSettings.settingsLoaded)
return
if (!GreetdSettings.weatherEnabled)
return
weatherInitialized = true
WeatherService.addRef()
WeatherService.forceRefresh()
}
Connections {
target: GreetdSettings
function onSettingsLoadedChanged() {
if (GreetdSettings.settingsLoaded)
initWeatherService()
}
}
Component.onCompleted: { Component.onCompleted: {
pickRandomFact() pickRandomFact()
WeatherService.addRef() initWeatherService()
if (isPrimaryScreen) { if (isPrimaryScreen) {
sessionListProc.running = true sessionListProc.running = true
applyLastSuccessfulUser() applyLastSuccessfulUser()
} }
if (CompositorService.isHyprland) { if (CompositorService.isHyprland)
updateHyprlandLayout() updateHyprlandLayout()
hyprlandLayoutUpdateTimer.start()
}
} }
function applyLastSuccessfulUser() { function applyLastSuccessfulUser() {
@@ -61,10 +83,8 @@ Item {
} }
Component.onDestruction: { Component.onDestruction: {
if (weatherInitialized)
WeatherService.removeRef() WeatherService.removeRef()
if (CompositorService.isHyprland) {
hyprlandLayoutUpdateTimer.stop()
}
} }
function updateHyprlandLayout() { function updateHyprlandLayout() {
@@ -106,14 +126,15 @@ Item {
} }
} }
Timer { Connections {
id: hyprlandLayoutUpdateTimer target: CompositorService.isHyprland ? Hyprland : null
interval: 1000 enabled: CompositorService.isHyprland
running: false
repeat: true
onTriggered: updateHyprlandLayout()
}
function onRawEvent(event) {
if (event.name === "activelayout")
updateHyprlandLayout()
}
}
Connections { Connections {
target: GreetdMemory target: GreetdMemory
@@ -750,13 +771,13 @@ Item {
visible: { visible: {
const keyboardVisible = (CompositorService.isNiri && NiriService.keyboardLayoutNames.length > 1) || const keyboardVisible = (CompositorService.isNiri && NiriService.keyboardLayoutNames.length > 1) ||
(CompositorService.isHyprland && hyprlandLayoutCount > 1) (CompositorService.isHyprland && hyprlandLayoutCount > 1)
return keyboardVisible && WeatherService.weather.available return keyboardVisible && GreetdSettings.weatherEnabled && WeatherService.weather.available
} }
} }
Row { Row {
spacing: 6 spacing: 6
visible: WeatherService.weather.available visible: GreetdSettings.weatherEnabled && WeatherService.weather.available
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
DankIcon { DankIcon {
@@ -780,7 +801,7 @@ Item {
height: 24 height: 24
color: Qt.rgba(255, 255, 255, 0.2) color: Qt.rgba(255, 255, 255, 0.2)
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
visible: WeatherService.weather.available && (NetworkService.networkStatus !== "disconnected" || BluetoothService.enabled || (AudioService.sink && AudioService.sink.audio) || BatteryService.batteryAvailable) visible: GreetdSettings.weatherEnabled && WeatherService.weather.available && (NetworkService.networkStatus !== "disconnected" || BluetoothService.enabled || (AudioService.sink && AudioService.sink.audio) || BatteryService.batteryAvailable)
} }
Row { Row {

View File

@@ -1110,7 +1110,7 @@ Item {
DankIcon { DankIcon {
name: "vpn_lock" name: "vpn_lock"
size: Theme.iconSize - 2 size: Theme.iconSize - 2
color: NetworkService.vpnConnected ? Theme.primary : Qt.rgba(255, 255, 255, 0.5) color: "white"
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
visible: NetworkService.vpnAvailable && NetworkService.vpnConnected visible: NetworkService.vpnAvailable && NetworkService.vpnConnected
} }

View File

@@ -124,7 +124,7 @@ DankPopout {
return Math.max(300, Math.min(baseHeight, maxHeight)); return Math.max(300, Math.min(baseHeight, maxHeight));
} }
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) color: "transparent"
radius: Theme.cornerRadius radius: Theme.cornerRadius
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 0 border.width: 0

View File

@@ -1,5 +1,4 @@
import QtQuick import QtQuick
import Quickshell.Wayland
import qs.Common import qs.Common
import qs.Widgets import qs.Widgets
@@ -25,7 +24,7 @@ DankPopout {
id: popoutContainer id: popoutContainer
implicitHeight: popoutColumn.implicitHeight + Theme.spacingL * 2 implicitHeight: popoutColumn.implicitHeight + Theme.spacingL * 2
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) color: "transparent"
radius: Theme.cornerRadius radius: Theme.cornerRadius
border.width: 0 border.width: 0
antialiasing: true antialiasing: true
@@ -34,14 +33,14 @@ DankPopout {
Component.onCompleted: { Component.onCompleted: {
if (root.shouldBeVisible) { if (root.shouldBeVisible) {
forceActiveFocus() forceActiveFocus();
} }
} }
Keys.onPressed: event => { Keys.onPressed: event => {
if (event.key === Qt.Key_Escape) { if (event.key === Qt.Key_Escape) {
root.close() root.close();
event.accepted = true event.accepted = true;
} }
} }
@@ -50,8 +49,8 @@ DankPopout {
function onShouldBeVisibleChanged() { function onShouldBeVisibleChanged() {
if (root.shouldBeVisible) { if (root.shouldBeVisible) {
Qt.callLater(() => { Qt.callLater(() => {
popoutContainer.forceActiveFocus() popoutContainer.forceActiveFocus();
}) });
} }
} }
} }
@@ -70,12 +69,12 @@ DankPopout {
onLoaded: { onLoaded: {
if (item && "closePopout" in item) { if (item && "closePopout" in item) {
item.closePopout = function() { item.closePopout = function () {
root.close() root.close();
} };
} }
if (item) { if (item) {
root.contentHeight = Qt.binding(() => item.implicitHeight + Theme.spacingS * 2) root.contentHeight = Qt.binding(() => item.implicitHeight + Theme.spacingS * 2);
} }
} }
} }

View File

@@ -1,11 +1,5 @@
import QtQuick import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import QtQuick.Layouts import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common import qs.Common
import qs.Modules.ProcessList import qs.Modules.ProcessList
import qs.Services import qs.Services
@@ -57,7 +51,7 @@ DankPopout {
id: processListContent id: processListContent
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) color: "transparent"
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 0 border.width: 0
clip: true clip: true
@@ -70,7 +64,7 @@ DankPopout {
} }
processContextMenu.parent = processListContent; processContextMenu.parent = processListContent;
} }
Keys.onPressed: (event) => { Keys.onPressed: event => {
if (event.key === Qt.Key_Escape) { if (event.key === Qt.Key_Escape) {
processListPopout.close(); processListPopout.close();
event.accepted = true; event.accepted = true;
@@ -108,7 +102,6 @@ DankPopout {
anchors.centerIn: parent anchors.centerIn: parent
width: parent.width - Theme.spacingM * 2 width: parent.width - Theme.spacingM * 2
} }
} }
Rectangle { Rectangle {
@@ -124,13 +117,8 @@ DankPopout {
anchors.margins: Theme.spacingS anchors.margins: Theme.spacingS
contextMenu: processContextMenu contextMenu: processContextMenu
} }
} }
} }
} }
} }
} }

View File

@@ -106,7 +106,7 @@ FloatingWindow {
minimumSize: Qt.size(450, 400) minimumSize: Qt.size(450, 400)
implicitWidth: 600 implicitWidth: 600
implicitHeight: 650 implicitHeight: 650
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) color: Theme.surfaceContainer
visible: false visible: false
onVisibleChanged: { onVisibleChanged: {
@@ -537,7 +537,7 @@ FloatingWindow {
title: I18n.tr("Third-Party Plugin Warning") title: I18n.tr("Third-Party Plugin Warning")
implicitWidth: 500 implicitWidth: 500
implicitHeight: 350 implicitHeight: 350
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) color: Theme.surfaceContainer
visible: false visible: false
FocusScope { FocusScope {

View File

@@ -93,7 +93,7 @@ FloatingWindow {
minimumSize: Qt.size(400, 350) minimumSize: Qt.size(400, 350)
implicitWidth: 500 implicitWidth: 500
implicitHeight: 550 implicitHeight: 550
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) color: Theme.surfaceContainer
visible: false visible: false
onVisibleChanged: { onVisibleChanged: {

View File

@@ -36,7 +36,7 @@ DankPopout {
Rectangle { Rectangle {
id: updaterPanel id: updaterPanel
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) color: "transparent"
radius: Theme.cornerRadius radius: Theme.cornerRadius
antialiasing: true antialiasing: true
smooth: true smooth: true

View File

@@ -5,6 +5,7 @@ import QtQuick
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import qs.Common import qs.Common
import qs.Modules.Greetd
import "../Common/suncalc.js" as SunCalc import "../Common/suncalc.js" as SunCalc
Singleton { Singleton {
@@ -509,10 +510,15 @@ Singleton {
} }
function updateLocation() { function updateLocation() {
if (SettingsData.useAutoLocation) { const useAuto = SessionData.isGreeterMode ? GreetdSettings.useAutoLocation : SettingsData.useAutoLocation;
const coords = SessionData.isGreeterMode ? GreetdSettings.weatherCoordinates : SettingsData.weatherCoordinates;
const cityName = SessionData.isGreeterMode ? GreetdSettings.weatherLocation : SettingsData.weatherLocation;
if (useAuto) {
getLocationFromIP(); getLocationFromIP();
} else { return;
const coords = SettingsData.weatherCoordinates; }
if (coords) { if (coords) {
const parts = coords.split(","); const parts = coords.split(",");
if (parts.length === 2) { if (parts.length === 2) {
@@ -525,12 +531,9 @@ Singleton {
} }
} }
const cityName = SettingsData.weatherLocation; if (cityName)
if (cityName) {
getLocationFromCity(cityName); getLocationFromCity(cityName);
} }
}
}
function getLocationFromCoords(lat, lon) { function getLocationFromCoords(lat, lon) {
const url = "https://nominatim.openstreetmap.org/reverse?lat=" + lat + "&lon=" + lon + "&format=json&addressdetails=1&accept-language=en"; const url = "https://nominatim.openstreetmap.org/reverse?lat=" + lat + "&lon=" + lon + "&format=json&addressdetails=1&accept-language=en";
@@ -867,7 +870,7 @@ Singleton {
Timer { Timer {
id: updateTimer id: updateTimer
interval: nextInterval() interval: nextInterval()
running: root.refCount > 0 && SettingsData.weatherEnabled running: root.refCount > 0 && SettingsData.weatherEnabled && !SessionData.isGreeterMode
repeat: true repeat: true
triggeredOnStart: true triggeredOnStart: true
onTriggered: { onTriggered: {

View File

@@ -46,11 +46,12 @@ Flickable {
lastWheelTime = currentTime; lastWheelTime = currentTime;
const hasPixel = event.pixelDelta && event.pixelDelta.y !== 0; const hasPixel = event.pixelDelta && event.pixelDelta.y !== 0;
const hasAngle = event.angleDelta && event.angleDelta.y !== 0;
const deltaY = event.angleDelta.y; const deltaY = event.angleDelta.y;
const isMouseWheel = !hasPixel && hasAngle; const isTraditionalMouse = !hasPixel && Math.abs(deltaY) >= 120 && (Math.abs(deltaY) % 120) === 0;
const isHighDpiMouse = !hasPixel && !isTraditionalMouse && deltaY !== 0;
const isTouchpad = hasPixel;
if (isMouseWheel) { if (isTraditionalMouse) {
sessionUsedMouseWheel = true; sessionUsedMouseWheel = true;
momentumTimer.stop(); momentumTimer.stop();
flickable.isMomentumActive = false; flickable.isMomentumActive = false;
@@ -58,7 +59,7 @@ Flickable {
momentum = 0; momentum = 0;
flickable.momentumVelocity = 0; flickable.momentumVelocity = 0;
const lines = Math.floor(Math.abs(deltaY) / 120); const lines = Math.round(Math.abs(deltaY) / 120);
const scrollAmount = (deltaY > 0 ? -lines : lines) * flickable.mouseWheelSpeed; const scrollAmount = (deltaY > 0 ? -lines : lines) * flickable.mouseWheelSpeed;
let newY = flickable.contentY + scrollAmount; let newY = flickable.contentY + scrollAmount;
newY = Math.max(0, Math.min(flickable.contentHeight - flickable.height, newY)); newY = Math.max(0, Math.min(flickable.contentHeight - flickable.height, newY));
@@ -68,17 +69,29 @@ Flickable {
} }
flickable.contentY = newY; flickable.contentY = newY;
} else { } else if (isHighDpiMouse) {
sessionUsedMouseWheel = true;
momentumTimer.stop();
flickable.isMomentumActive = false;
velocitySamples = [];
momentum = 0;
flickable.momentumVelocity = 0;
let delta = deltaY / 8 * touchpadSpeed;
let newY = flickable.contentY - delta;
newY = Math.max(0, Math.min(flickable.contentHeight - flickable.height, newY));
if (flickable.flicking) {
flickable.cancelFlick();
}
flickable.contentY = newY;
} else if (isTouchpad) {
sessionUsedMouseWheel = false; sessionUsedMouseWheel = false;
momentumTimer.stop(); momentumTimer.stop();
flickable.isMomentumActive = false; flickable.isMomentumActive = false;
let delta = 0; let delta = event.pixelDelta.y * touchpadSpeed;
if (event.pixelDelta.y !== 0) {
delta = event.pixelDelta.y * touchpadSpeed;
} else {
delta = event.angleDelta.y / 8 * touchpadSpeed;
}
velocitySamples.push({ velocitySamples.push({
"delta": delta, "delta": delta,
@@ -94,7 +107,7 @@ Flickable {
} }
} }
if (event.pixelDelta.y !== 0 && timeDelta < 50) { if (timeDelta < 50) {
momentum = momentum * momentumRetention + delta * 0.15; momentum = momentum * momentumRetention + delta * 0.15;
delta += momentum; delta += momentum;
} else { } else {

View File

@@ -50,11 +50,12 @@ GridView {
lastWheelTime = currentTime; lastWheelTime = currentTime;
const hasPixel = event.pixelDelta && event.pixelDelta.y !== 0; const hasPixel = event.pixelDelta && event.pixelDelta.y !== 0;
const hasAngle = event.angleDelta && event.angleDelta.y !== 0;
const deltaY = event.angleDelta.y; const deltaY = event.angleDelta.y;
const isMouseWheel = !hasPixel && hasAngle; const isTraditionalMouse = !hasPixel && Math.abs(deltaY) >= 120 && (Math.abs(deltaY) % 120) === 0;
const isHighDpiMouse = !hasPixel && !isTraditionalMouse && deltaY !== 0;
const isTouchpad = hasPixel;
if (isMouseWheel) { if (isTraditionalMouse) {
sessionUsedMouseWheel = true; sessionUsedMouseWheel = true;
momentumTimer.stop(); momentumTimer.stop();
isMomentumActive = false; isMomentumActive = false;
@@ -62,7 +63,7 @@ GridView {
momentum = 0; momentum = 0;
momentumVelocity = 0; momentumVelocity = 0;
const lines = Math.floor(Math.abs(deltaY) / 120); const lines = Math.round(Math.abs(deltaY) / 120);
const scrollAmount = (deltaY > 0 ? -lines : lines) * cellHeight * 0.35; const scrollAmount = (deltaY > 0 ? -lines : lines) * cellHeight * 0.35;
let newY = contentY + scrollAmount; let newY = contentY + scrollAmount;
newY = Math.max(0, Math.min(contentHeight - height, newY)); newY = Math.max(0, Math.min(contentHeight - height, newY));
@@ -72,12 +73,29 @@ GridView {
} }
contentY = newY; contentY = newY;
} else { } else if (isHighDpiMouse) {
sessionUsedMouseWheel = true;
momentumTimer.stop();
isMomentumActive = false;
velocitySamples = [];
momentum = 0;
momentumVelocity = 0;
let delta = deltaY / 120 * cellHeight * 1.2;
let newY = contentY - delta;
newY = Math.max(0, Math.min(contentHeight - height, newY));
if (flicking) {
cancelFlick();
}
contentY = newY;
} else if (isTouchpad) {
sessionUsedMouseWheel = false; sessionUsedMouseWheel = false;
momentumTimer.stop(); momentumTimer.stop();
isMomentumActive = false; isMomentumActive = false;
let delta = event.pixelDelta.y !== 0 ? event.pixelDelta.y * touchpadSpeed : event.angleDelta.y / 120 * cellHeight * 1.2; let delta = event.pixelDelta.y * touchpadSpeed;
velocitySamples.push({ velocitySamples.push({
"delta": delta, "delta": delta,
@@ -93,7 +111,7 @@ GridView {
} }
} }
if (event.pixelDelta.y !== 0 && timeDelta < 50) { if (timeDelta < 50) {
momentum = momentum * momentumRetention + delta * 0.15; momentum = momentum * momentumRetention + delta * 0.15;
delta += momentum; delta += momentum;
} else { } else {

View File

@@ -69,11 +69,12 @@ ListView {
lastWheelTime = currentTime; lastWheelTime = currentTime;
const hasPixel = event.pixelDelta && event.pixelDelta.y !== 0; const hasPixel = event.pixelDelta && event.pixelDelta.y !== 0;
const hasAngle = event.angleDelta && event.angleDelta.y !== 0;
const deltaY = event.angleDelta.y; const deltaY = event.angleDelta.y;
const isMouseWheel = !hasPixel && hasAngle; const isTraditionalMouse = !hasPixel && Math.abs(deltaY) >= 120 && (Math.abs(deltaY) % 120) === 0;
const isHighDpiMouse = !hasPixel && !isTraditionalMouse && deltaY !== 0;
const isTouchpad = hasPixel;
if (isMouseWheel) { if (isTraditionalMouse) {
sessionUsedMouseWheel = true; sessionUsedMouseWheel = true;
momentumTimer.stop(); momentumTimer.stop();
isMomentumActive = false; isMomentumActive = false;
@@ -81,7 +82,7 @@ ListView {
momentum = 0; momentum = 0;
momentumVelocity = 0; momentumVelocity = 0;
const lines = Math.floor(Math.abs(deltaY) / 120); const lines = Math.round(Math.abs(deltaY) / 120);
const scrollAmount = (deltaY > 0 ? -lines : lines) * mouseWheelSpeed; const scrollAmount = (deltaY > 0 ? -lines : lines) * mouseWheelSpeed;
let newY = listView.contentY + scrollAmount; let newY = listView.contentY + scrollAmount;
const maxY = Math.max(0, listView.contentHeight - listView.height + listView.originY); const maxY = Math.max(0, listView.contentHeight - listView.height + listView.originY);
@@ -93,17 +94,31 @@ ListView {
listView.contentY = newY; listView.contentY = newY;
savedY = newY; savedY = newY;
} else { } else if (isHighDpiMouse) {
sessionUsedMouseWheel = true;
momentumTimer.stop();
isMomentumActive = false;
velocitySamples = [];
momentum = 0;
momentumVelocity = 0;
let delta = deltaY / 8 * touchpadSpeed;
let newY = listView.contentY - delta;
const maxY = Math.max(0, listView.contentHeight - listView.height + listView.originY);
newY = Math.max(listView.originY, Math.min(maxY, newY));
if (listView.flicking) {
listView.cancelFlick();
}
listView.contentY = newY;
savedY = newY;
} else if (isTouchpad) {
sessionUsedMouseWheel = false; sessionUsedMouseWheel = false;
momentumTimer.stop(); momentumTimer.stop();
isMomentumActive = false; isMomentumActive = false;
let delta = 0; let delta = event.pixelDelta.y * touchpadSpeed;
if (event.pixelDelta.y !== 0) {
delta = event.pixelDelta.y * touchpadSpeed;
} else {
delta = event.angleDelta.y / 8 * touchpadSpeed;
}
velocitySamples.push({ velocitySamples.push({
"delta": delta, "delta": delta,
@@ -119,7 +134,7 @@ ListView {
} }
} }
if (event.pixelDelta.y !== 0 && timeDelta < 50) { if (timeDelta < 50) {
momentum = momentum * 0.92 + delta * 0.15; momentum = momentum * 0.92 + delta * 0.15;
delta += momentum; delta += momentum;
} else { } else {

View File

@@ -396,7 +396,6 @@ Item {
Item { Item {
id: bgShadowLayer id: bgShadowLayer
anchors.fill: parent anchors.fill: parent
visible: contentWrapper.popupSurfaceAlpha >= 0.95
layer.enabled: Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1" layer.enabled: Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1"
layer.smooth: false layer.smooth: false
layer.textureSize: Qt.size(Math.round(width * root.dpr), Math.round(height * root.dpr)) layer.textureSize: Qt.size(Math.round(width * root.dpr), Math.round(height * root.dpr))
@@ -421,6 +420,7 @@ Item {
DankRectangle { DankRectangle {
anchors.fill: parent anchors.fill: parent
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
} }
} }