1package app
2
3import (
4 "math"
5 "os"
6 "strconv"
7 "sync"
8
9 "github.com/bnema/neferwl/internal/adapters/xcursor"
10 "github.com/bnema/neferwl/internal/ports"
11)
12
13// cursorSize is XCURSOR_SIZE (logical pixels), 24 by default.
14func cursorSize() int {
15 if n, err := strconv.Atoi(os.Getenv("XCURSOR_SIZE")); err == nil && n >= 8 && n <= 256 {
16 return n
17 }
18 return 24
19}
20
21// legacyCursorNames are the X11 names of CSS cursors for themes without
22// the CSS ones.
23var legacyCursorNames = map[string][]string{
24 "default": {"left_ptr"},
25 "pointer": {"hand2", "hand1"},
26 "text": {"xterm"},
27 "wait": {"watch"},
28 "progress": {"left_ptr_watch"},
29 "crosshair": {"cross"},
30 "move": {"fleur"},
31 "grab": {"openhand"},
32 "grabbing": {"closedhand"},
33 "not-allowed": {"crossed_circle"},
34 "help": {"question_arrow"},
35 "ew-resize": {"sb_h_double_arrow"},
36 "ns-resize": {"sb_v_double_arrow"},
37 "col-resize": {"sb_h_double_arrow"},
38 "row-resize": {"sb_v_double_arrow"},
39 "e-resize": {"right_side"},
40 "w-resize": {"left_side"},
41 "n-resize": {"top_side"},
42 "s-resize": {"bottom_side"},
43 "ne-resize": {"top_right_corner"},
44 "nw-resize": {"top_left_corner"},
45 "se-resize": {"bottom_right_corner"},
46 "sw-resize": {"bottom_left_corner"},
47 "nesw-resize": {"fd_double_arrow"},
48 "nwse-resize": {"bd_double_arrow"},
49 "all-scroll": {"fleur"},
50 "alias": {"link"},
51 "cell": {"plus"},
52 "no-drop": {"crossed_circle"},
53}
54
55// themeCursors caches loaded theme cursors: clients change shapes on every
56// hover and each output loads them on its render goroutine.
57var themeCursors = struct {
58 sync.Mutex
59 m map[themeKey]ports.CursorImage
60}{m: map[themeKey]ports.CursorImage{}}
61
62type themeKey struct {
63 shape string
64 size, limit int
65}
66
67// loadCursor returns the cursor a client asked for at an output scale, at
68// most limit physical pixels on a side: a XCURSOR_THEME shape at
69// XCURSOR_SIZE × scale, the client image resized to the output scale, or
70// an empty image when hidden. Unknown shapes fall back to the arrow.
71func loadCursor(c ports.CursorChange, scale float64, limit int) (ports.CursorImage, error) {
72 scale = max(scale, 1)
73 if c.Hidden {
74 return ports.CursorImage{}, nil
75 }
76 if c.Image != nil {
77 img := xcursor.Image{W: c.Image.W, H: c.Image.H, HotX: c.Image.HotX, HotY: c.Image.HotY, Pixels: c.Image.Pixels}
78 f := scale / float64(max(c.Scale, 1))
79 w, h := int(math.Round(float64(img.W)*f)), int(math.Round(float64(img.H)*f))
80 if big := max(w, h); big > limit {
81 w, h = w*limit/big, h*limit/big
82 }
83 img = img.Scale(max(w, 1), max(h, 1))
84 return ports.CursorImage{W: img.W, H: img.H, HotX: img.HotX, HotY: img.HotY, Pixels: img.Pixels}, nil
85 }
86 shape := c.Shape
87 if shape == "" {
88 shape = "default"
89 }
90 size := int(math.Round(float64(cursorSize()) * scale))
91 key := themeKey{shape, size, limit}
92 themeCursors.Lock()
93 defer themeCursors.Unlock()
94 if img, ok := themeCursors.m[key]; ok {
95 return img, nil
96 }
97 // The shape is looked up through the whole theme chain before the
98 // arrow: a theme may override only the arrow and inherit the rest.
99 dirs, theme := xcursor.SearchPath(), os.Getenv("XCURSOR_THEME")
100 img, err := xcursor.Load(dirs, theme, append([]string{shape}, legacyCursorNames[shape]...), size)
101 if err != nil && shape != "default" {
102 img, err = xcursor.Load(dirs, theme, []string{"default", "left_ptr"}, size)
103 }
104 if err != nil {
105 return ports.CursorImage{}, err
106 }
107 img = img.ForSize(size, limit)
108 out := ports.CursorImage{W: img.W, H: img.H, HotX: img.HotX, HotY: img.HotY, Pixels: img.Pixels}
109 themeCursors.m[key] = out
110 return out, nil
111}