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, then rotated by the output transform t (the
71// cursor plane and the headless cursor are drawn in target space). Unknown
72// shapes fall back to the arrow.
73func loadCursor(c ports.CursorChange, scale float64, t ports.BufferTransform, limit int) (ports.CursorImage, error) {
74 img, err := loadCursorUpright(c, scale, limit)
75 if err != nil {
76 return img, err
77 }
78 return rotateCursor(img, t), nil
79}
80
81// rotateCursor returns img as an output with transform t holds it, hotspot
82// included. It never modifies img (theme images are cached).
83func rotateCursor(img ports.CursorImage, t ports.BufferTransform) ports.CursorImage {
84 if t == 0 || img.W <= 0 || img.H <= 0 || len(img.Pixels) < img.W*img.H*4 {
85 return img
86 }
87 dw, dh := t.Size(img.W, img.H)
88 out := make([]byte, dw*dh*4)
89 for y := range img.H {
90 for x := range img.W {
91 bx, by := t.ToBuffer(float64(x)+.5, float64(y)+.5, float64(img.W), float64(img.H))
92 src := (y*img.W + x) * 4
93 copy(out[(int(by)*dw+int(bx))*4:], img.Pixels[src:src+4])
94 }
95 }
96 hx, hy := t.ToBuffer(float64(img.HotX)+.5, float64(img.HotY)+.5, float64(img.W), float64(img.H))
97 return ports.CursorImage{W: dw, H: dh, HotX: int(hx), HotY: int(hy), Pixels: out}
98}
99
100func loadCursorUpright(c ports.CursorChange, scale float64, limit int) (ports.CursorImage, error) {
101 scale = max(scale, 1)
102 if c.Hidden {
103 return ports.CursorImage{}, nil
104 }
105 if c.Image != nil {
106 img := xcursor.Image{W: c.Image.W, H: c.Image.H, HotX: c.Image.HotX, HotY: c.Image.HotY, Pixels: c.Image.Pixels}
107 f := scale / float64(max(c.Scale, 1))
108 w, h := int(math.Round(float64(img.W)*f)), int(math.Round(float64(img.H)*f))
109 if big := max(w, h); big > limit {
110 w, h = w*limit/big, h*limit/big
111 }
112 img = img.Scale(max(w, 1), max(h, 1))
113 return ports.CursorImage{W: img.W, H: img.H, HotX: img.HotX, HotY: img.HotY, Pixels: img.Pixels}, nil
114 }
115 shape := c.Shape
116 if shape == "" {
117 shape = "default"
118 }
119 size := int(math.Round(float64(cursorSize()) * scale))
120 key := themeKey{shape, size, limit}
121 themeCursors.Lock()
122 defer themeCursors.Unlock()
123 if img, ok := themeCursors.m[key]; ok {
124 return img, nil
125 }
126 // The shape is looked up through the whole theme chain before the
127 // arrow: a theme may override only the arrow and inherit the rest.
128 dirs, theme := xcursor.SearchPath(), os.Getenv("XCURSOR_THEME")
129 img, err := xcursor.Load(dirs, theme, append([]string{shape}, legacyCursorNames[shape]...), size)
130 if err != nil && shape != "default" {
131 img, err = xcursor.Load(dirs, theme, []string{"default", "left_ptr"}, size)
132 }
133 if err != nil {
134 return ports.CursorImage{}, err
135 }
136 img = img.ForSize(size, limit)
137 out := ports.CursorImage{W: img.W, H: img.H, HotX: img.HotX, HotY: img.HotY, Pixels: img.Pixels}
138 themeCursors.m[key] = out
139 return out, nil
140}