Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
verify/clients/src/bin/xembed_icon.rs (9.5K)
1 // xembed-icon — a legacy X11 tray icon, the way Wine's systray docks one.
2 //
3 // Waits for an owner of `_NET_SYSTEM_TRAY_S0`, creates a window in the
4 // visual the tray advertises (`_NET_SYSTEM_TRAY_VISUAL`, ARGB when there is
5 // one), sets `_XEMBED_INFO` mapped, and sends SYSTEM_TRAY_REQUEST_DOCK. It
6 // paints itself one solid colour on every Expose. One milestone per line:
7 //
8 // tray owner 0xWIN
9 // window 0xWIN depth D
10 // docked into 0xPARENT (ReparentNotify away from the root)
11 // embedded (XEMBED_EMBEDDED_NOTIFY)
12 // button N press|release x,y root rx,ry
13 // recolored 0xAARRGGBB
14 // undocked (reparented back to the root)
15 // popup at X,Y WxH (--popup: opened, then after any move)
16 // popup dismissed by press at X,Y
17 //
18 // Args: --color 0xAARRGGBB (premultiplied pixel, default opaque red)
19 // --recolor SECS 0xAARRGGBB repaint in a second colour later
20 // --exit-after SECS destroy the window and exit
21 // --size N window size, default 32
22 // --popup WxH on a right-click, open an override-
23 // redirect popup the way a Windows tray
24 // app does: bottom-aligned at the click's
25 // root point and clamped to the screen
26 // top; closed by a press outside it
27
28 use std::time::{Duration, Instant};
29
30 use x11rb::connection::Connection;
31 use x11rb::protocol::xproto::*;
32 use x11rb::protocol::Event;
33 use x11rb::wrapper::ConnectionExt as _;
34 use x11rb::{CURRENT_TIME, NONE};
35
36 fn parse_hex(s: &str) -> u32 {
37 u32::from_str_radix(s.trim_start_matches("0x"), 16).expect("hex colour")
38 }
39
40 fn main() {
41 let mut color = 0xffd0_2020u32;
42 let mut recolor: Option<(f64, u32)> = None;
43 let mut exit_after: Option<f64> = None;
44 let mut size: u16 = 32;
45 let mut popup_size: Option<(u16, u16)> = None;
46 let mut args = std::env::args().skip(1);
47 while let Some(a) = args.next() {
48 match a.as_str() {
49 "--color" => color = parse_hex(&args.next().unwrap()),
50 "--recolor" => {
51 let secs = args.next().unwrap().parse().unwrap();
52 recolor = Some((secs, parse_hex(&args.next().unwrap())));
53 }
54 "--exit-after" => exit_after = Some(args.next().unwrap().parse().unwrap()),
55 "--size" => size = args.next().unwrap().parse().unwrap(),
56 "--popup" => {
57 let v = args.next().unwrap();
58 let (w, h) = v.split_once('x').expect("WxH");
59 popup_size = Some((w.parse().unwrap(), h.parse().unwrap()));
60 }
61 other => panic!("unknown arg {other}"),
62 }
63 }
64
65 let (conn, screen_num) = x11rb::connect(None).expect("X connection");
66 let screen = conn.setup().roots[screen_num].clone();
67 let atom = |name: &str| conn.intern_atom(false, name.as_bytes()).unwrap().reply().unwrap().atom;
68 let selection = atom(&format!("_NET_SYSTEM_TRAY_S{screen_num}"));
69 let opcode = atom("_NET_SYSTEM_TRAY_OPCODE");
70 let tray_visual = atom("_NET_SYSTEM_TRAY_VISUAL");
71 let xembed = atom("_XEMBED");
72 let xembed_info = atom("_XEMBED_INFO");
73
74 let deadline = Instant::now() + Duration::from_secs(20);
75 let owner = loop {
76 let owner = conn.get_selection_owner(selection).unwrap().reply().unwrap().owner;
77 if owner != NONE {
78 break owner;
79 }
80 assert!(Instant::now() < deadline, "no tray appeared");
81 std::thread::sleep(Duration::from_millis(200));
82 };
83 println!("tray owner {owner:#x}");
84
85 // The tray's visual when it names one (and it is on this screen).
86 let advertised = conn
87 .get_property(false, owner, tray_visual, AtomEnum::VISUALID, 0, 1)
88 .unwrap()
89 .reply()
90 .ok()
91 .and_then(|r| r.value32().and_then(|mut v| v.next()));
92 let (depth, visual) = advertised
93 .and_then(|vid| {
94 screen.allowed_depths.iter().find_map(|d| {
95 d.visuals.iter().any(|v| v.visual_id == vid).then_some((d.depth, vid))
96 })
97 })
98 .unwrap_or((screen.root_depth, screen.root_visual));
99 let colormap = conn.generate_id().unwrap();
100 conn.create_colormap(ColormapAlloc::NONE, colormap, screen.root, visual).unwrap();
101
102 let win = conn.generate_id().unwrap();
103 conn.create_window(
104 depth,
105 win,
106 screen.root,
107 0,
108 0,
109 size,
110 size,
111 0,
112 WindowClass::INPUT_OUTPUT,
113 visual,
114 &CreateWindowAux::new()
115 .background_pixel(0)
116 .border_pixel(0)
117 .colormap(colormap)
118 .event_mask(
119 EventMask::EXPOSURE
120 | EventMask::BUTTON_PRESS
121 | EventMask::BUTTON_RELEASE
122 | EventMask::STRUCTURE_NOTIFY,
123 ),
124 )
125 .unwrap();
126 conn.change_property8(PropMode::REPLACE, win, AtomEnum::WM_NAME, AtomEnum::STRING, b"xembed-icon test")
127 .unwrap();
128 conn.change_property8(
129 PropMode::REPLACE,
130 win,
131 AtomEnum::WM_CLASS,
132 AtomEnum::STRING,
133 b"xembed-icon\0XembedIcon\0",
134 )
135 .unwrap();
136 conn.change_property32(PropMode::REPLACE, win, xembed_info, xembed_info, &[0, 1]).unwrap();
137 let gc = conn.generate_id().unwrap();
138 conn.create_gc(gc, win, &CreateGCAux::new().foreground(color)).unwrap();
139 println!("window {win:#x} depth {depth}");
140
141 let dock = ClientMessageEvent::new(32, owner, opcode, [CURRENT_TIME, 0, win, 0, 0]);
142 conn.send_event(false, owner, EventMask::NO_EVENT, dock).unwrap();
143 conn.flush().unwrap();
144
145 let mut popup: Option<(Window, u16, u16)> = None;
146 let start = Instant::now();
147 let paint = |c: u32| {
148 conn.change_gc(gc, &ChangeGCAux::new().foreground(c)).unwrap();
149 conn.poly_fill_rectangle(win, gc, &[Rectangle { x: 0, y: 0, width: size, height: size }]).unwrap();
150 conn.flush().unwrap();
151 };
152 loop {
153 let elapsed = start.elapsed().as_secs_f64();
154 if let Some((at, c)) = recolor {
155 if elapsed >= at {
156 color = c;
157 paint(color);
158 println!("recolored {color:#010x}");
159 recolor = None;
160 }
161 }
162 if exit_after.is_some_and(|at| elapsed >= at) {
163 conn.destroy_window(win).unwrap();
164 conn.flush().unwrap();
165 println!("exiting");
166 return;
167 }
168 while let Some(event) = conn.poll_for_event().unwrap() {
169 match event {
170 Event::Expose(e) if e.count == 0 => paint(color),
171 Event::ReparentNotify(e) if e.window == win => {
172 if e.parent == screen.root {
173 println!("undocked");
174 } else {
175 println!("docked into {:#x}", e.parent);
176 }
177 }
178 Event::ClientMessage(e) if e.type_ == xembed => {
179 if e.data.as_data32()[1] == 0 {
180 println!("embedded");
181 }
182 }
183 Event::ButtonPress(e) if popup.is_some_and(|(p, _, _)| p == e.event) => {
184 let (p, w, h) = popup.unwrap();
185 let inside = e.event_x >= 0 && e.event_y >= 0 && e.event_x < w as i16 && e.event_y < h as i16;
186 if !inside {
187 conn.destroy_window(p).unwrap();
188 conn.flush().unwrap();
189 popup = None;
190 println!("popup dismissed by press at {},{}", e.event_x, e.event_y);
191 }
192 }
193 Event::ConfigureNotify(e) if popup.is_some_and(|(p, _, _)| p == e.window) => {
194 println!("popup at {},{} {}x{}", e.x, e.y, e.width, e.height);
195 }
196 Event::ButtonPress(e) => {
197 println!("button {} press {},{} root {},{}", e.detail, e.event_x, e.event_y, e.root_x, e.root_y);
198 if let (3, Some((w, h)), None) = (e.detail, popup_size, popup) {
199 let x = (e.root_x - w as i16 / 2).max(0);
200 let y = (e.root_y - h as i16).max(0);
201 let p = conn.generate_id().unwrap();
202 conn.create_window(
203 screen.root_depth,
204 p,
205 screen.root,
206 x,
207 y,
208 w,
209 h,
210 0,
211 WindowClass::INPUT_OUTPUT,
212 screen.root_visual,
213 &CreateWindowAux::new()
214 .background_pixel(screen.white_pixel)
215 .override_redirect(1)
216 .event_mask(EventMask::BUTTON_PRESS | EventMask::STRUCTURE_NOTIFY),
217 )
218 .unwrap();
219 conn.map_window(p).unwrap();
220 conn.flush().unwrap();
221 popup = Some((p, w, h));
222 println!("popup at {x},{y} {w}x{h}");
223 }
224 }
225 Event::ButtonRelease(e) => {
226 println!("button {} release {},{} root {},{}", e.detail, e.event_x, e.event_y, e.root_x, e.root_y)
227 }
228 _ => {}
229 }
230 }
231 std::thread::sleep(Duration::from_millis(20));
232 }
233 }