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,