Commit dac55c3
8
files
+322
added
-84
deleted
@@ -263,6 +263,8 @@ pub struct App {
263
263
pub last_click: Option<(usize, Instant)>,
264
264
pub last_mount_refresh: Instant,
265
265
pub status: String,
266
+ /// Zakładka pod kursorem (podświetlenie w pasku tytułu).
267
+ pub tab_hover: Option<usize>,
266
268
pub pending_close: bool,
267
269
pub pending_minimize: bool,
268
270
pub pending_maximize: bool,
@@ -298,6 +300,7 @@ impl App {
298
300
last_click: None,
299
301
last_mount_refresh: Instant::now() - Duration::from_secs(10),
300
302
status: String::new(),
303
+ tab_hover: None,
301
304
pending_close: false,
302
305
pending_minimize: false,
303
306
pending_maximize: false,
@@ -23,8 +23,6 @@ use crate::places;
23
23
use crate::trash;
24
24
25
25
const SIDEBAR_W: f64 = 216.0;
26
-const TABS_H: f64 = 34.0;
27
-const NAV_H: f64 = 40.0;
28
26
const STATUS_H: f64 = 28.0;
29
27
const PREVIEW_W: f64 = 300.0;
30
28
const PAD: f64 = 8.0;
@@ -32,8 +30,15 @@ const BTN: f64 = 28.0;
32
30
const PANE_PATH_H: f64 = 26.0;
33
31
34
32
struct Layout {
33
+ /// Cały pasek tytułu: jeden wiersz albo dwa (gdy otwarte są zakładki).
35
34
header: Rect,
35
+ /// Górny wiersz paska — kontrolki okna i zakładki.
36
+ top: Rect,
37
+ /// Pasek zakładek wewnątrz `HeaderBar` (jego obszar `content`).
36
38
tabs: Rect,
39
+ /// Przycisk „nowa zakładka” obok zakładek (tylko przy >1 zakładce).
40
+ plus: Rect,
41
+ /// Wiersz nawigacji (przy jednej zakładce to ten sam wiersz co `top`).
37
42
nav: Rect,
38
43
sidebar: Rect,
39
44
status: Rect,
@@ -41,13 +46,13 @@ struct Layout {
41
46
}
42
47
43
48
struct Chrome {
49
+ sidebar_toggle: Rect,
44
50
back: Rect,
45
51
forward: Rect,
46
52
up: Rect,
47
53
search: Rect,
48
54
view: Rect,
49
55
menu: Rect,
50
- plus: Rect,
51
56
path: Rect,
52
57
}
53
58
@@ -75,11 +80,58 @@ fn hit_index(pane: &Pane, rows: Rect, p: Point, theme: &Theme) -> Option<usize>
75
80
}
76
81
}
77
82
78
-fn layout(app: &App, size: Size, theme: &Theme) -> Layout {
83
+/// Pasek zakładek z bieżącego stanu aplikacji.
84
+fn tab_bar(app: &App) -> TabBar {
85
+ let pane = app.pane();
86
+ let tabs: Vec<Tab> = pane
87
+ .tabs
88
+ .iter()
89
+ .map(|p| Tab::new(tab_title(p)).closable(true))
90
+ .collect();
91
+ let mut bar = TabBar::with_tabs(tabs).expand(false);
92
+ bar.set_selected(pane.tab);
93
+ bar.set_hovered(app.tab_hover);
94
+ bar
95
+}
96
+
97
+fn layout(app: &App, size: Size, theme: &Theme, text: &TextEngine) -> Layout {
79
98
let hh = theme.metrics.headerbar.height;
80
- let header = Rect::from_xywh(0.0, 0.0, size.width, hh);
81
- let tabs = Rect::from_xywh(0.0, hh, size.width, TABS_H);
82
- let nav = Rect::from_xywh(0.0, tabs.bottom(), size.width, NAV_H);
99
+ let multi = app.pane().tabs.len() > 1;
100
+ // Pasek jest **jeden**: nawigacja siedzi w tym samym wierszu co kontrolki okna.
101
+ // Dopiero przy wielu zakładkach dochodzi drugi wiersz (na górze), bo tam jest
102
+ // ich miejsce — dokładnie jak w GNOME Files.
103
+ let header_h = if multi { hh * 2.0 } else { hh };
104
+ let header = Rect::from_xywh(0.0, 0.0, size.width, header_h);
105
+ let top = Rect::from_xywh(0.0, 0.0, size.width, hh);
106
+ let nav = if multi {
107
+ Rect::from_xywh(0.0, hh, size.width, hh)
108
+ } else {
109
+ top
110
+ };
111
+ // Zakładki zajmują `content` górnego wiersza — między krawędzią a kontrolkami okna.
112
+ let content = HeaderBar::new()
113
+ .controls(ControlsSide::Right)
114
+ .controls_layout(top, theme)
115
+ .content;
116
+ let tabs_w = (content.width() - BTN - 4.0).max(1.0);
117
+ let tab_h = theme.metrics.tabs.height.min(content.height());
118
+ let tabs = Rect::from_xywh(content.x(), content.center().y - tab_h / 2.0, tabs_w, tab_h);
119
+ // „+” tuż za ostatnią zakładką (jak w przeglądarce), ale nigdy pod kontrolkami okna.
120
+ let plus = if multi {
121
+ let last_right = tab_bar(app)
122
+ .segments(tabs, text, theme)
123
+ .last()
124
+ .map(|r| r.right())
125
+ .unwrap_or(tabs.x());
126
+ Rect::from_xywh(
127
+ (last_right + 6.0).min(content.right() - BTN),
128
+ content.center().y - BTN / 2.0,
129
+ BTN,
130
+ BTN,
131
+ )
132
+ } else {
133
+ Rect::ZERO
134
+ };
83
135
let status = Rect::from_xywh(
84
136
0.0,
85
137
(size.height - STATUS_H).max(nav.bottom()),
@@ -109,7 +161,9 @@ fn layout(app: &App, size: Size, theme: &Theme) -> Layout {
109
161
}
110
162
Layout {
111
163
header,
164
+ top,
112
165
tabs,
166
+ plus,
113
167
nav,
114
168
sidebar,
115
169
status,
@@ -117,19 +171,31 @@ fn layout(app: &App, size: Size, theme: &Theme) -> Layout {
117
171
}
118
172
}
119
173
120
-fn chrome(lay: &Layout, size: Size) -> Chrome {
174
+fn chrome(app: &App, lay: &Layout, theme: &Theme) -> Chrome {
175
+ let multi = app.pane().tabs.len() > 1;
176
+ // Przy jednej zakładce nawigacja dzieli wiersz z kontrolkami okna, więc kończymy
177
+ // przed nimi. Przy dwóch wierszach dolny jest wolny na całej szerokości.
178
+ let (left, right) = if multi {
179
+ (lay.nav.x() + PAD, lay.nav.right() - PAD)
180
+ } else {
181
+ let content = HeaderBar::new()
182
+ .controls(ControlsSide::Right)
183
+ .controls_layout(lay.top, theme)
184
+ .content;
185
+ (content.x(), content.right())
186
+ };
121
187
let cy = lay.nav.center().y;
122
- let mut x = lay.nav.x() + PAD;
188
+ let mut x = left;
123
189
let btn = |x: &mut f64| {
124
190
let r = Rect::from_xywh(*x, cy - BTN / 2.0, BTN, BTN);
125
191
*x += BTN + 2.0;
126
192
r
127
193
};
194
+ let sidebar_toggle = btn(&mut x);
128
195
let back = btn(&mut x);
129
196
let forward = btn(&mut x);
130
197
let up = btn(&mut x);
131
198
132
- let right = lay.nav.right() - PAD;
133
199
let menu = Rect::from_xywh(right - BTN, cy - BTN / 2.0, BTN, BTN);
134
200
let view = Rect::from_xywh(menu.x() - BTN - 4.0, cy - BTN / 2.0, BTN, BTN);
135
201
let search = Rect::from_xywh(view.x() - BTN - 4.0, cy - BTN / 2.0, BTN, BTN);
@@ -137,22 +203,16 @@ fn chrome(lay: &Layout, size: Size) -> Chrome {
137
203
up.right() + PAD,
138
204
lay.nav.y() + 5.0,
139
205
(search.x() - PAD - (up.right() + PAD)).max(40.0),
140
- NAV_H - 10.0,
141
- );
142
- let plus = Rect::from_xywh(
143
- (size.width - PAD - BTN).max(0.0),
144
- lay.tabs.center().y - BTN / 2.0,
145
- BTN,
146
- BTN,
206
+ (lay.nav.height() - 10.0).max(20.0),
147
207
);
148
208
Chrome {
209
+ sidebar_toggle,
149
210
back,
150
211
forward,
151
212
up,
152
213
search,
153
214
view,
154
215
menu,
155
- plus,
156
216
path,
157
217
}
158
218
}
@@ -215,16 +275,15 @@ fn pane_rects(app: &App, lay: &Layout, i: usize, theme: &Theme) -> PaneRects {
215
275
// ---------------------------------------------------------------------------
216
276
217
277
pub fn paint(app: &App, scene: &mut Scene, size: Size, theme: &Theme, text: &TextEngine) {
218
- let lay = layout(app, size, theme);
219
- let ch = chrome(&lay, size);
278
+ let lay = layout(app, size, theme, text);
279
+ let ch = chrome(app, &lay, theme);
220
280
let mut ctx = PaintCtx::new(scene, theme, text);
221
281
222
282
ctx.scene
223
283
.fill_rect(Rect::from_size(size), theme.colors.window_bg);
224
284
225
285
paint_header(&mut ctx, app, &lay);
226
- paint_tabs(&mut ctx, app, &lay, &ch);
227
- paint_nav(&mut ctx, app, &lay, &ch);
286
+ paint_nav(&mut ctx, app, &ch);
228
287
paint_sidebar(&mut ctx, app, &lay);
229
288
paint_panes(&mut ctx, app, &lay);
230
289
paint_status(&mut ctx, app, &lay);
@@ -242,37 +301,54 @@ pub fn paint(app: &App, scene: &mut Scene, size: Size, theme: &Theme, text: &Tex
242
301
}
243
302
244
303
fn paint_header(ctx: &mut PaintCtx, app: &App, lay: &Layout) {
245
- let title = if app.pane().dir.path == Path::new("/") {
246
- libluna_i18n::tr("File System")
247
- } else {
248
- app.pane().dir.title()
249
- };
250
- let header = HeaderBar::new().title(title).controls(ControlsSide::Right);
251
- header.paint(ctx, lay.header, None);
304
+ let multi = app.pane().tabs.len() > 1;
305
+ // Jeden pasek: tło obejmuje też dolny wiersz, gdy są zakładki.
306
+ ctx.scene
307
+ .fill_rect(lay.header, ctx.theme.colors.headerbar_bg);
308
+ // Kontrolki okna rysuje widget — bez tytułu, bo w jednym wierszu jest pasek ścieżki.
309
+ HeaderBar::new()
310
+ .controls(ControlsSide::Right)
311
+ .paint(ctx, lay.top, None);
312
+ if multi {
313
+ // Oba wiersze mają wyglądać jak jeden pasek: zamalowuj linię pod górnym.
314
+ ctx.scene.fill_rect(
315
+ Rect::from_xywh(lay.top.x(), lay.top.bottom() - 1.0, lay.top.width(), 1.0),
316
+ ctx.theme.colors.headerbar_bg,
317
+ );
318
+ paint_tabs(ctx, app, lay);
319
+ }
320
+ // Jedna linia pod całym paskiem (nie dwie).
321
+ ctx.scene.fill_rect(
322
+ Rect::from_xywh(
323
+ lay.header.x(),
324
+ lay.header.bottom() - 1.0,
325
+ lay.header.width(),
326
+ 1.0,
327
+ ),
328
+ ctx.theme.colors.border,
329
+ );
252
330
}
253
331
254
-fn paint_tabs(ctx: &mut PaintCtx, app: &App, lay: &Layout, ch: &Chrome) {
255
- let pane = app.pane();
256
- let tabs: Vec<Tab> = pane
257
- .tabs
258
- .iter()
259
- .map(|p| Tab::new(tab_title(p)).closable(pane.tabs.len() > 1))
260
- .collect();
261
- let mut bar = TabBar::with_tabs(tabs).expand(false);
262
- bar.set_selected(pane.tab);
263
- let rect = Rect::from_xywh(
264
- lay.tabs.x(),
265
- lay.tabs.y(),
266
- (ch.plus.x() - lay.tabs.x() - PAD).max(1.0),
267
- lay.tabs.height(),
268
- );
269
- bar.paint(ctx, rect);
270
- icon_button(ctx, ch.plus, IconName::Plus, true, false);
332
+fn paint_tabs(ctx: &mut PaintCtx, app: &App, lay: &Layout) {
333
+ tab_bar(app).paint(ctx, lay.tabs);
334
+ icon_button(ctx, lay.plus, IconName::Plus, true, false);
271
335
}
272
336
273
-fn paint_nav(ctx: &mut PaintCtx, app: &App, lay: &Layout, ch: &Chrome) {
337
+fn paint_nav(ctx: &mut PaintCtx, app: &App, ch: &Chrome) {
274
338
let pane = app.pane();
275
- ctx.scene.fill_rect(lay.nav, ctx.theme.colors.headerbar_bg);
339
+
340
+ // Przełącznik paska bocznego (jak w aplikacjach libLuny).
341
+ ctx.scene.fill_rounded_rect(
342
+ ch.sidebar_toggle,
343
+ ctx.theme.radius.small,
344
+ ctx.theme.colors.shade,
345
+ );
346
+ sidebar_icon(
347
+ ctx,
348
+ ctx.theme.colors.text,
349
+ ch.sidebar_toggle,
350
+ app.sidebar_visible,
351
+ );
276
352
277
353
icon_button(
278
354
ctx,
@@ -578,6 +654,18 @@ fn icon_button(ctx: &mut PaintCtx, rect: Rect, name: IconName, enabled: bool, ac
578
654
icon::paint_icon(ctx, name, rect.deflate(Insets::splat(6.0)), color);
579
655
}
580
656
657
+/// Ikona przełącznika paska bocznego (◧ / ▭) — jak w pozostałych aplikacjach Luny.
658
+fn sidebar_icon(ctx: &mut PaintCtx, color: Color, box_: Rect, filled: bool) {
659
+ let c = box_.center();
660
+ let (w, h) = (18.0, 14.0);
661
+ let outer = Rect::from_xywh(c.x - w / 2.0, c.y - h / 2.0, w, h);
662
+ ctx.scene.stroke_rounded_rect(outer, 3.0, 1.5, color);
663
+ if filled {
664
+ let inner = Rect::from_xywh(outer.x() + 2.0, outer.y() + 2.0, w * 0.36, h - 4.0);
665
+ ctx.scene.fill_rounded_rect(inner, 2.0, color);
666
+ }
667
+}
668
+
581
669
fn icon_up(ctx: &mut PaintCtx, rect: Rect, enabled: bool) {
582
670
let color = if enabled {
583
671
ctx.theme.colors.text
@@ -681,8 +769,8 @@ pub fn handle_events(
681
769
theme: &Theme,
682
770
text: &TextEngine,
683
771
) {
684
- let lay = layout(app, size, theme);
685
- let ch = chrome(&lay, size);
772
+ let lay = layout(app, size, theme, text);
773
+ let ch = chrome(app, &lay, theme);
686
774
687
775
// Modal konfliktu blokuje resztę interfejsu.
688
776
if app.conflict.is_some() {
@@ -756,6 +844,12 @@ pub fn handle_events(
756
844
context.menu.hover_at(context.panel, Some(p), theme);
757
845
continue;
758
846
}
847
+ let tab_hover = if app.pane().tabs.len() > 1 && lay.tabs.contains(p) {
848
+ tab_bar(app).index_at(lay.tabs, text, theme, p)
849
+ } else {
850
+ None
851
+ };
852
+ app.tab_hover = tab_hover;
759
853
let _hovered =
760
854
app.sidebar
761
855
.index_at(lay.sidebar.deflate(Insets::splat(6.0)), p, theme);
@@ -771,6 +865,7 @@ pub fn handle_events(
771
865
}
772
866
}
773
867
Event::PointerLeave => {
868
+ app.tab_hover = None;
774
869
app.sidebar.set_hovered(None);
775
870
for pane in &mut app.panes {
776
871
pane.view.set_hovered(None);
@@ -827,10 +922,10 @@ fn on_left_press(
827
922
return;
828
923
}
829
924
830
- // 2. Kontrolki okna (nasz HeaderBar).
925
+ // 2. Kontrolki okna (górny wiersz paska).
831
926
let header = HeaderBar::new().controls(ControlsSide::Right);
832
- if lay.header.contains(p) {
833
- if let Some(control) = header.control_at(lay.header, theme, p) {
927
+ if lay.top.contains(p) {
928
+ if let Some(control) = header.control_at(lay.top, theme, p) {
834
929
match control {
835
930
WindowControl::Close => app.pending_close = true,
836
931
WindowControl::Minimize => app.pending_minimize = true,
@@ -838,45 +933,32 @@ fn on_left_press(
838
933
}
839
934
return;
840
935
}
841
- // Przeciąganie za wolny obszar paska.
842
- let content = header.controls_layout(lay.header, theme).content;
843
- if content.contains(p) {
844
- app.pending_move = true;
845
- }
846
- return;
847
936
}
848
937
849
- // 3. Zakładki.
850
- if lay.tabs.contains(p) {
851
- if ch.plus.contains(p) {
938
+ // 3. Zakładki (tylko gdy jest ich więcej niż jedna).
939
+ if app.pane().tabs.len() > 1 && lay.top.contains(p) {
940
+ if lay.plus.contains(p) {
852
941
app.new_tab();
853
942
return;
854
943
}
855
- let pane = app.pane();
856
- let tabs: Vec<Tab> = pane
857
- .tabs
858
- .iter()
859
- .map(|tp| Tab::new(tab_title(tp)).closable(pane.tabs.len() > 1))
860
- .collect();
861
- let mut bar = TabBar::with_tabs(tabs).expand(false);
862
- bar.set_selected(pane.tab);
863
- let rect = Rect::from_xywh(
864
- lay.tabs.x(),
865
- lay.tabs.y(),
866
- (ch.plus.x() - lay.tabs.x() - PAD).max(1.0),
867
- lay.tabs.height(),
868
- );
869
- if let Some(i) = bar.close_at(rect, text, theme, p) {
944
+ let bar = tab_bar(app);
945
+ if let Some(i) = bar.close_at(lay.tabs, text, theme, p) {
870
946
app.select_tab(i);
871
947
app.close_tab();
872
- } else if let Some(i) = bar.index_at(rect, text, theme, p) {
948
+ return;
949
+ }
950
+ if let Some(i) = bar.index_at(lay.tabs, text, theme, p) {
873
951
app.select_tab(i);
952
+ return;
874
953
}
875
- return;
876
954
}
877
955
878
- // 4. Pasek nawigacji.
956
+ // 4. Pasek nawigacji (przy jednej zakładce w tym samym wierszu co kontrolki).
879
957
if lay.nav.contains(p) {
958
+ if ch.sidebar_toggle.contains(p) {
959
+ app.sidebar_visible = !app.sidebar_visible;
960
+ return;
961
+ }
880
962
if ch.back.contains(p) {
881
963
app.back();
882
964
return;
@@ -929,7 +1011,18 @@ fn on_left_press(
929
1011
}
930
1012
}
931
1013
932
- // 5. Panel boczny.
1014
+ // 5. Przeciąganie okna za wolne miejsce paska (poza przyciskami i ścieżką).
1015
+ if lay.header.contains(p) {
1016
+ if lay.top.contains(p) {
1017
+ let content = header.controls_layout(lay.top, theme).content;
1018
+ if content.contains(p) {
1019
+ app.pending_move = true;
1020
+ }
1021
+ }
1022
+ return;
1023
+ }
1024
+
1025
+ // 6. Panel boczny.
933
1026
if lay.sidebar.contains(p) {
934
1027
let entry = app
935
1028
.sidebar
@@ -264,9 +264,24 @@ impl Dispatch<wl_surface::WlSurface, SurfaceData> for State {
264
264
crate::subsurface::repaint_parent(state, &sub.parent);
265
265
}
266
266
// Okno w modelu chowamy (nie usuwamy — indeks slotu musi zostać stabilny).
267
- if let Some(slot) = *data.slot.lock_or_recover() {
267
+ // Ważne: guard `data.slot` zwalniamy **przed** `clear_focus_if` — inaczej
268
+ // `broadcast_states` próbuje zamknąć ten sam mutex i wątek wisi.
269
+ let slot = *data.slot.lock_or_recover();
270
+ if let Some(slot) = slot {
268
271
if let Some(win) = state.windows.lock_or_recover().get_mut(slot) {
269
272
win.mapped = false;
273
+ win.minimized = false;
274
+ win.maximized = false;
275
+ win.fullscreen = false;
276
+ win.input_capture = false;
277
+ win.input = None;
278
+ // Zwolnij bufor ostatniej klatki (może mieć kilka MB).
279
+ win.data = std::sync::Arc::new(ImageData::empty(1, 1));
280
+ }
281
+ // Fokus nie może wskazywać na powierzchnię, której już nie ma.
282
+ crate::seat::clear_focus_if(state, slot);
283
+ if std::env::var("SELENE_STATS").is_ok() {
284
+ eprintln!("selene/win: powierzchnia zniszczona slot={slot}");
270
285
}
271
286
}
272
287
// Pasek (foreign-toplevel) dowiaduje się, że okno zniknęło.
@@ -12,6 +12,7 @@ use std::path::PathBuf;
12
12
use std::time::Duration;
13
13
14
14
use crate::state::State;
15
+use crate::sync::LockExt;
15
16
16
17
pub struct Control {
17
18
listener: UnixListener,
@@ -60,6 +61,29 @@ fn handle(state: &mut State, line: &str) {
60
61
crate::seat::move_window_by_title(state, title, ws);
61
62
}
62
63
}
64
+ // `windows` — diagnostyka: lista okien w modelu kompozycji.
65
+ "windows" => {
66
+ let ws = *state.ws_current.lock_or_recover();
67
+ let wins = state.windows.lock_or_recover();
68
+ eprintln!("selene/control: okna={} ws={ws}", wins.len());
69
+ for (i, w) in wins.iter().enumerate() {
70
+ eprintln!(
71
+ "selene/control: [{i}] mapped={} min={} popup={} layer={} z={} \"{}\" {}x{} @ ({:.0},{:.0}) data={}x{}",
72
+ w.mapped,
73
+ w.minimized,
74
+ w.popup,
75
+ w.layer,
76
+ w.z,
77
+ w.title,
78
+ w.width,
79
+ w.height,
80
+ w.x,
81
+ w.y,
82
+ w.data.width,
83
+ w.data.height
84
+ );
85
+ }
86
+ }
63
87
// `screenshot` — zapis klatek wszystkich monitorów (Super+F12).
64
88
"screenshot" => {
65
89
if std::env::var("SELENE_STATS").is_ok() {
@@ -188,6 +188,7 @@ fn main() -> Result<(), Box<dyn Error>> {
188
188
wake_write,
189
189
Arc::clone(&state.ws_current),
190
190
Arc::clone(&state.overview),
191
+ Arc::clone(&state.screenshot),
191
192
ws_count,
192
193
1280,
193
194
800,
@@ -16,7 +16,9 @@ use libluna_widgets::{ControlsSide, HeaderBar, PaintCtx};
16
16
use crate::drm::{DrmBackend, DrmError};
17
17
use crate::input::{self, Events};
18
18
use crate::monitors::{layout_horizontal, OutputInfo, Outputs};
19
-use crate::state::{CursorModel, Overview, OverviewModel, Window, WindowModel, WorkspaceModel};
19
+use crate::state::{
20
+ CursorModel, Overview, OverviewModel, ScreenshotModel, Window, WindowModel, WorkspaceModel,
21
+};
20
22
use crate::sync::LockExt;
21
23
22
24
/// Uruchamia wątek wyjścia (nested Wayland) o zadanym rozmiarze.
@@ -27,6 +29,7 @@ pub fn spawn(
27
29
wake: UnixStream,
28
30
ws: WorkspaceModel,
29
31
overview: OverviewModel,
32
+ screenshot: ScreenshotModel,
30
33
ws_count: usize,
31
34
width: u32,
32
35
height: u32,
@@ -71,6 +74,23 @@ pub fn spawn(
71
74
&theme,
72
75
&text,
73
76
);
77
+ // Zrzut ekranu także w trybie nested: kompozytor nie ma tu skanouta,
78
+ // więc kodujemy bieżącą scenę bezpośrednio do PNG.
79
+ if screenshot.swap(false, std::sync::atomic::Ordering::Relaxed) {
80
+ let mut renderer = SoftwareRenderer::new(size.width as u32, size.height as u32);
81
+ renderer.draw(frame.scene, frame.scale, Color::TRANSPARENT);
82
+ if let Some(png) = renderer.encode_png() {
83
+ let stamp = std::time::SystemTime::now()
84
+ .duration_since(std::time::UNIX_EPOCH)
85
+ .map(|d| d.as_secs())
86
+ .unwrap_or(0);
87
+ let path = screenshot_dir().join(format!("luna-{stamp}-nested.png"));
88
+ match std::fs::write(&path, png) {
89
+ Ok(()) => eprintln!("selene: zrzut ekranu: {}", path.display()),
90
+ Err(e) => eprintln!("selene: nie udało się zapisać zrzutu: {e}"),
91
+ }
92
+ }
93
+ }
74
94
// Utrzymuj klatki — model okien zmienia się poza tą pętlą.
75
95
frame.request_frame();
76
96
});
@@ -715,6 +735,16 @@ fn build_scene(
715
735
// przeświecało przez nie jako ramka wokół okna.
716
736
// `buffer_rect()` uwzględnia cienie CSD (`set_window_geometry`) — bufor
717
737
// jest większy od widocznego okna i musi być rysowany z przesunięciem.
738
+ // Zaokrąglone narożniki: wymuszamy dla wszystkich zwykłych okien (także CSD),
739
+ // promieniem z motywu. Maksymalizowane/fullscreen zostają ostre.
740
+ let radius = if !w.popup && !w.layer && !w.maximized && !w.fullscreen {
741
+ theme.radius.large
742
+ } else {
743
+ 0.0
744
+ };
745
+ if radius > 0.0 {
746
+ scene.push_clip_rounded(sh(w.outer()), radius);
747
+ }
718
748
scene.image(sh(w.buffer_rect()), w.data.clone());
719
749
// Pasek tytułu rysujemy **tylko** oknom z naszym SSD (`w.ssd`). Klienty
720
750
// z własnymi dekoracjami (GTK/libadwaita, CSD) nie mają `ssd`, a wcześniej
@@ -747,6 +777,9 @@ fn build_scene(
747
777
// (z własną linią pod nim). Dodatkowy `stroke` wyglądał jak obca ramka
748
778
// od innego środowiska, zwłaszcza na oknach X11.
749
779
}
780
+ if radius > 0.0 {
781
+ scene.pop_clip();
782
+ }
750
783
}
751
784
}
752
785
@@ -1429,7 +1429,7 @@ fn x11_window(state: &State, slot: usize) -> Option<u32> {
1429
1429
}
1430
1430
1431
1431
/// Zdejmuje fokus klawiatury/wskaźnika z okna (np. po minimalizacji).
1432
-fn clear_focus_if(state: &mut State, slot: usize) {
1432
+pub(crate) fn clear_focus_if(state: &mut State, slot: usize) {
1433
1433
if state.keyboard_focus.as_ref().map(|(s, _)| *s) == Some(slot) {
1434
1434
if let Some((_, surface)) = state.keyboard_focus.clone() {
1435
1435
if let Some(kb) = keyboard_for(state, &surface) {
@@ -73,6 +73,19 @@ impl X11Info {
73
73
pub enum X11Change {
74
74
Fullscreen(bool),
75
75
Maximized(bool),
76
+ /// Klient prosi o zminimalizowanie/przywrócenie (`WM_CHANGE_STATE` / IconicState).
77
+ Minimize(bool),
78
+ /// Interaktywne przesunięcie od pozycji wskaźnika (`_NET_WM_MOVERESIZE`, kierunek 8).
79
+ Move {
80
+ x: i32,
81
+ y: i32,
82
+ },
83
+ /// Interaktywna zmiana rozmiaru (bitmaska jak w `seat`: 1=góra, 2=dół, 4=lewo, 8=prawo).
84
+ Resize {
85
+ edges: u8,
86
+ x: i32,
87
+ y: i32,
88
+ },
76
89
}
77
90
78
91
/// Polecenia z kompozytora do wątku XWM.
@@ -576,6 +589,31 @@ fn handle_event(
576
589
st.ewmh.insert(e.window, flags);
577
590
st.changes
578
591
.extend(pending.into_iter().map(|c| (e.window, c)));
592
+ } else if e.type_ == atoms.wm_change_state {
593
+ // `WM_CHANGE_STATE`: 1 = NormalState, 3 = IconicState (minimalizacja).
594
+ let data = e.data.as_data32();
595
+ let iconic = data[0] == 3;
596
+ state
597
+ .lock_or_recover()
598
+ .changes
599
+ .push((e.window, X11Change::Minimize(iconic)));
600
+ } else if e.type_ == atoms.net_wm_moveresize {
601
+ // `data = [x_root, y_root, direction, button, source]`.
602
+ let data = e.data.as_data32();
603
+ let (x, y, direction) = (data[0] as i32, data[1] as i32, data[2]);
604
+ let change = match direction {
605
+ // 8 = Move, 0..=7 = krawędzie/narożniki, 11 = Cancel.
606
+ 8 => Some(X11Change::Move { x, y }),
607
+ 0..=7 => Some(X11Change::Resize {
608
+ edges: move_resize_edges(direction),
609
+ x,
610
+ y,
611
+ }),
612
+ _ => None,
613
+ };
614
+ if let Some(change) = change {
615
+ state.lock_or_recover().changes.push((e.window, change));
616
+ }
579
617
}
580
618
}
581
619
Event::UnmapNotify(e) => {
@@ -656,6 +694,21 @@ fn apply_command(
656
694
}
657
695
658
696
/// Odczytuje serial z komunikatu `WL_SURFACE_SERIAL` (dwa razy 32 bity: `lo`, `hi`).
697
+/// Bitmaska krawędzi dla kierunku `_NET_WM_MOVERESIZE` (1=góra, 2=dół, 4=lewo, 8=prawo).
698
+fn move_resize_edges(direction: u32) -> u8 {
699
+ match direction {
700
+ 0 => 1 | 4, // lewy-górny
701
+ 1 => 1, // góra
702
+ 2 => 1 | 8, // prawy-górny
703
+ 3 => 8, // prawo
704
+ 4 => 2 | 8, // prawy-dolny
705
+ 5 => 2, // dół
706
+ 6 => 2 | 4, // lewy-dolny
707
+ 7 => 4, // lewo
708
+ _ => 0,
709
+ }
710
+}
711
+
659
712
fn serial_from_client_message(data: &x11rb::protocol::xproto::ClientMessageData) -> u64 {
660
713
let raw = data.serialize();
661
714
if raw.len() < 8 {
@@ -868,6 +921,8 @@ struct Atoms {
868
921
net_wm_window_type_notification: Atom,
869
922
net_frame_extents: Atom,
870
923
wl_surface_serial: Atom,
924
+ wm_change_state: Atom,
925
+ net_wm_moveresize: Atom,
871
926
}
872
927
873
928
impl Atoms {
@@ -895,6 +950,8 @@ impl Atoms {
895
950
net_wm_window_type_notification: atom(conn, "_NET_WM_WINDOW_TYPE_NOTIFICATION")?,
896
951
net_frame_extents: atom(conn, "_NET_FRAME_EXTENTS")?,
897
952
wl_surface_serial: atom(conn, "WL_SURFACE_SERIAL")?,
953
+ wm_change_state: atom(conn, "WM_CHANGE_STATE")?,
954
+ net_wm_moveresize: atom(conn, "_NET_WM_MOVERESIZE")?,
898
955
})
899
956
}
900
957
}
@@ -924,6 +981,18 @@ pub fn sync(state: &mut State) {
924
981
match change {
925
982
X11Change::Fullscreen(on) => crate::seat::set_fullscreen(state, slot, on),
926
983
X11Change::Maximized(on) => crate::seat::set_maximized(state, slot, on),
984
+ X11Change::Minimize(on) => crate::seat::set_minimized(state, slot, on),
985
+ X11Change::Move { x, y } => crate::seat::interactive_move_at(
986
+ state,
987
+ slot,
988
+ libluna_core::Point::new(x as f64, y as f64),
989
+ ),
990
+ X11Change::Resize { edges, x, y } => crate::seat::interactive_resize_at(
991
+ state,
992
+ slot,
993
+ edges,
994
+ libluna_core::Point::new(x as f64, y as f64),
995
+ ),
927
996
}
928
997
}
929
998
}