← luna

Commit eb337fc

5
files
+157
added
-7
deleted
@@ -2,6 +2,7 @@
2 2
3 3 use std::sync::Mutex;
4 4
5 +use libluna_core::Rect;
5 6 use libluna_render::ImageData;
6 7 use wayland_protocols::xdg::shell::server::{xdg_popup, xdg_surface, xdg_toplevel};
7 8 use wayland_protocols_wlr::layer_shell::v1::server::zwlr_layer_surface_v1;
@@ -30,6 +31,8 @@ pub struct SurfaceData {
30 31 pub layer: Mutex<Option<zwlr_layer_surface_v1::ZwlrLayerSurfaceV1>>,
31 32 /// Uchwyty `zwlr_foreign_toplevel` (gdy to okno najwyższego poziomu).
32 33 pub toplevel: Mutex<Option<crate::foreign_toplevel::ToplevelInfo>>,
34 + /// Region wejścia (`wl_surface.set_input_region`); `None` = cała powierzchnia.
35 + pub input: Mutex<Option<Vec<Rect>>>,
33 36 }
34 37
35 38 impl SurfaceData {
@@ -41,6 +44,7 @@ impl SurfaceData {
41 44 slot: Mutex::new(None),
42 45 layer: Mutex::new(None),
43 46 toplevel: Mutex::new(None),
47 + input: Mutex::new(None),
44 48 }
45 49 }
46 50 }
@@ -80,23 +84,44 @@ impl Dispatch<wl_compositor::WlCompositor, ()> for State {
80 84 state.surfaces += 1;
81 85 }
82 86 wl_compositor::Request::CreateRegion { id } => {
83 - data_init.init(id, ());
87 + data_init.init(id, RegionRects::default());
84 88 }
85 89 _ => {}
86 90 }
87 91 }
88 92 }
89 93
90 -impl Dispatch<wl_region::WlRegion, ()> for State {
94 +/// Prostokąty regionu `wl_region` (współdzielone; klient może je zmieniać).
95 +pub type RegionRects = std::sync::Arc<Mutex<Vec<Rect>>>;
96 +
97 +impl Dispatch<wl_region::WlRegion, RegionRects> for State {
91 98 fn request(
92 99 _state: &mut Self,
93 100 _client: &Client,
94 101 _resource: &wl_region::WlRegion,
95 - _request: wl_region::Request,
96 - _data: &(),
102 + request: wl_region::Request,
103 + rects: &RegionRects,
97 104 _handle: &DisplayHandle,
98 105 _data_init: &mut DataInit<'_, Self>,
99 106 ) {
107 + let mut r = rects.lock_or_recover();
108 + match request {
109 + wl_region::Request::Add {
110 + x,
111 + y,
112 + width,
113 + height,
114 + } => r.push(Rect::from_xywh(
115 + x as f64,
116 + y as f64,
117 + width as f64,
118 + height as f64,
119 + )),
120 + // `subtract` upraszczamy (rzadkie w regionach wejścia).
121 + wl_region::Request::Subtract { .. } => {}
122 + wl_region::Request::Destroy => r.clear(),
123 + _ => {}
124 + }
100 125 }
101 126 }
102 127
@@ -142,6 +167,12 @@ impl Dispatch<wl_surface::WlSurface, SurfaceData> for State {
142 167 wl_surface::Request::Attach { buffer, .. } => {
143 168 *data.buffer.lock_or_recover() = buffer;
144 169 }
170 + wl_surface::Request::SetInputRegion { region } => {
171 + // `None` = cała powierzchnia; inaczej migawka prostokątów regionu.
172 + *data.input.lock_or_recover() = region
173 + .as_ref()
174 + .and_then(|r| r.data::<RegionRects>().map(|m| m.lock_or_recover().clone()));
175 + }
145 176 wl_surface::Request::Commit => {
146 177 // Wywołaj oczekujące frame-callbacki.
147 178 let now = state.time_ms();
@@ -275,10 +306,13 @@ fn composite_layer(
275 306 return;
276 307 };
277 308 let image = std::sync::Arc::new(ImageData::new(w as u32, h as u32, rgba));
309 + // Region wejścia wyprowadzony z przezroczystości: pasek (nieprzezroczysty) łapie
310 + // kliki, a przezroczysty obszar chromu przepuszcza je do okien pod spodem.
311 + let input_rects = opaque_input_rects(&image);
278 312 let st = *ld.state.lock_or_recover();
279 313 let (rx, ry) = st.position(out_w, out_h, w as f64, h as f64);
280 314 let z = crate::layer::layer_z(st.layer);
281 - let (_, created) = upsert_window(
315 + let (slot, created) = upsert_window(
282 316 state,
283 317 surface,
284 318 data,
@@ -293,6 +327,15 @@ fn composite_layer(
293 327 true,
294 328 image,
295 329 );
330 + {
331 + let mut windows = state.windows.lock_or_recover();
332 + if let Some(win) = windows.get_mut(slot) {
333 + // Region klienta ma priorytet; inaczej nasz z alfy.
334 + if win.input.is_none() {
335 + win.input = Some(input_rects);
336 + }
337 + }
338 + }
296 339 if debug && created {
297 340 eprintln!(
298 341 "selene/layer: okno utworzone {}x{} @ ({:.0},{:.0}), z={}",
@@ -306,6 +349,86 @@ fn composite_layer(
306 349 let _ = buffer.send_event(wl_buffer::Event::Release);
307 350 }
308 351
352 +/// Region wejścia z kanału alfa: nieprzezroczyste bloki scalane poziomo w prostokąty.
353 +fn opaque_input_rects(image: &ImageData) -> Vec<Rect> {
354 + const BLOCK: usize = 16;
355 + const ALPHA_THRESHOLD: u8 = 8;
356 + let iw = image.width as usize;
357 + let ih = image.height as usize;
358 + if iw == 0 || ih == 0 || image.rgba.len() < iw * ih * 4 {
359 + return Vec::new();
360 + }
361 + let bw = iw.div_ceil(BLOCK);
362 + let bh = ih.div_ceil(BLOCK);
363 + let mut rects = Vec::new();
364 + for by in 0..bh {
365 + let y0 = by * BLOCK;
366 + let y1 = (y0 + BLOCK).min(ih);
367 + let mut run: Option<usize> = None;
368 + for bx in 0..bw {
369 + let x0 = bx * BLOCK;
370 + let x1 = (x0 + BLOCK).min(iw);
371 + let mut opaque = false;
372 + 'scan: for y in y0..y1 {
373 + let row = y * iw * 4;
374 + for x in x0..x1 {
375 + if image.rgba[row + x * 4 + 3] > ALPHA_THRESHOLD {
376 + opaque = true;
377 + break 'scan;
378 + }
379 + }
380 + }
381 + match (run, opaque) {
382 + (None, true) => run = Some(bx),
383 + (Some(s), false) => {
384 + rects.push(block_run(s, bx, by, BLOCK, iw, ih));
385 + run = None;
386 + }
387 + _ => {}
388 + }
389 + }
390 + if let Some(s) = run {
391 + rects.push(block_run(s, bw, by, BLOCK, iw, ih));
392 + }
393 + }
394 + rects
395 +}
396 +
397 +/// Prostokąt dla ciągu bloków `[bx0, bx1)` w rzędzie `by`.
398 +fn block_run(bx0: usize, bx1: usize, by: usize, block: usize, iw: usize, ih: usize) -> Rect {
399 + let x = bx0 * block;
400 + let y = by * block;
401 + let w = (bx1 * block).min(iw).saturating_sub(x);
402 + let h = (y + block).min(ih) - y;
403 + Rect::from_xywh(x as f64, y as f64, w as f64, h as f64)
404 +}
405 +
406 +#[cfg(test)]
407 +mod input_region_tests {
408 + use super::*;
409 +
410 + #[test]
411 + fn opaque_rects_track_alpha() {
412 + // 32×32, nieprzezroczysta tylko lewa połowa (blok 16 px).
413 + let mut rgba = vec![0u8; 32 * 32 * 4];
414 + for y in 0..32 {
415 + for x in 0..16 {
416 + rgba[(y * 32 + x) * 4 + 3] = 255;
417 + }
418 + }
419 + let rects = opaque_input_rects(&ImageData::new(32, 32, rgba));
420 + assert!(!rects.is_empty());
421 + assert!(rects.iter().all(|r| r.x() < 16.0));
422 + assert!(rects.iter().all(|r| r.x() + r.width() <= 16.001));
423 + }
424 +
425 + #[test]
426 + fn fully_transparent_yields_no_rects() {
427 + let rects = opaque_input_rects(&ImageData::new(40, 40, vec![0u8; 40 * 40 * 4]));
428 + assert!(rects.is_empty());
429 + }
430 +}
431 +
309 432 /// Dodaje nowe okno lub aktualizuje istniejące (slot trzymany w [`SurfaceData`]).
310 433 #[allow(clippy::too_many_arguments)]
311 434 fn upsert_window(
@@ -325,6 +448,7 @@ fn upsert_window(
325 448 ) -> (usize, bool) {
326 449 let mut slot = data.slot.lock_or_recover();
327 450 let ws = *state.ws_current.lock_or_recover();
451 + let input = data.input.lock_or_recover().clone();
328 452 let mut windows = state.windows.lock_or_recover();
329 453 let mut created = false;
330 454 let idx = match *slot {
@@ -344,6 +468,7 @@ fn upsert_window(
344 468 if !title.is_empty() {
345 469 win.title = title.to_owned();
346 470 }
471 + win.input = input.clone();
347 472 win.data = image;
348 473 }
349 474 i
@@ -361,6 +486,7 @@ fn upsert_window(
361 486 layer,
362 487 mapped: true,
363 488 workspace: ws,
489 + input,
364 490 minimized: false,
365 491 maximized: false,
366 492 fullscreen: false,
@@ -323,6 +323,7 @@ mod tests {
323 323 layer: false,
324 324 mapped: true,
325 325 workspace: 0,
326 + input: None,
326 327 minimized: true,
327 328 maximized: true,
328 329 fullscreen: false,
@@ -642,6 +642,7 @@ mod tests {
642 642 layer: true,
643 643 mapped: true,
644 644 workspace: 0,
645 + input: None,
645 646 minimized: false,
646 647 maximized: false,
647 648 fullscreen: false,
@@ -184,7 +184,11 @@ fn hit(state: &State, pos: Point) -> Option<(usize, Part)> {
184 184 let cur_ws = *state.ws_current.lock_or_recover();
185 185 let mut best: Option<(usize, i32)> = None;
186 186 for (i, w) in windows.iter().enumerate() {
187 - if w.mapped && !w.minimized && w.in_workspace(cur_ws) && w.outer().contains(pos) {
187 + let inside = match input_region_hit(w, pos) {
188 + Some(h) => h,
189 + None => w.outer().contains(pos),
190 + };
191 + if w.mapped && !w.minimized && w.in_workspace(cur_ws) && inside {
188 192 match best {
189 193 Some((_, z)) if z >= w.z => {}
190 194 _ => best = Some((i, w.z)),
@@ -218,7 +222,11 @@ fn content_window(state: &State, pos: Point) -> Option<usize> {
218 222 let cur_ws = *state.ws_current.lock_or_recover();
219 223 let mut best: Option<(usize, i32)> = None;
220 224 for (i, w) in windows.iter().enumerate() {
221 - if w.mapped && !w.minimized && w.in_workspace(cur_ws) && w.content().contains(pos) {
225 + let inside = match input_region_hit(w, pos) {
226 + Some(h) => h,
227 + None => w.content().contains(pos),
228 + };
229 + if w.mapped && !w.minimized && w.in_workspace(cur_ws) && inside {
222 230 match best {
223 231 Some((_, z)) if z >= w.z => {}
224 232 _ => best = Some((i, w.z)),
@@ -276,6 +284,16 @@ pub(crate) fn window_at(state: &State, pos: (f64, f64)) -> Option<usize> {
276 284 content_window(state, Point::new(pos.0, pos.1))
277 285 }
278 286
287 +/// Trafienie w region wejścia powierzchni warstwowej; `None` = brak regionu (cała).
288 +fn input_region_hit(w: &crate::state::Window, pos: Point) -> Option<bool> {
289 + if !w.layer {
290 + return None;
291 + }
292 + w.input
293 + .as_ref()
294 + .map(|rects| rects.iter().any(|r| r.offset(w.x, w.y).contains(pos)))
295 +}
296 +
279 297 fn window_geometry(state: &State, slot: usize) -> Option<(f64, f64, f64, f64)> {
280 298 let windows = state.windows.lock_or_recover();
281 299 windows
@@ -1240,6 +1258,7 @@ mod tests {
1240 1258 layer: false,
1241 1259 mapped: true,
1242 1260 workspace: 0,
1261 + input: None,
1243 1262 minimized: false,
1244 1263 maximized: false,
1245 1264 fullscreen: false,
@@ -55,6 +55,8 @@ pub struct Window {
55 55 pub mapped: bool,
56 56 /// Przestrzeń robocza, do której należy okno (panele/popupy są globalne).
57 57 pub workspace: usize,
58 + /// Region wejścia powierzchni (`wl_surface.set_input_region`); `None` = cała.
59 + pub input: Option<Vec<Rect>>,
58 60 /// Zminimalizowane (ukryte przez kompozytor; przywracane skrótem).
59 61 pub minimized: bool,
60 62 /// Zmaksymalizowane do obszaru monitora.
@@ -465,6 +467,7 @@ mod tests {
465 467 layer: false,
466 468 mapped: true,
467 469 workspace: ws,
470 + input: None,
468 471 minimized: false,
469 472 maximized: false,
470 473 fullscreen: false,