git.lucas.co / cce-compositor
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 }