← luna

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 }