Commit 28d62a5
6
plików
+326
dodanych
-32
usuniętych
@@ -22,7 +22,10 @@ use smithay::{
22
22
desktop::{layer_map_for_output, WindowSurfaceType},
23
23
input::{
24
24
keyboard::{FilterResult, Keysym, KeysymHandle, ModifiersState},
25
- pointer::{AxisFrame, ButtonEvent, MotionEvent, RelativeMotionEvent},
25
+ pointer::{
26
+ AxisFrame, ButtonEvent, Focus, GrabStartData as PointerGrabStartData, MotionEvent,
27
+ RelativeMotionEvent,
28
+ },
26
29
},
27
30
output::Output,
28
31
reexports::wayland_server::protocol::wl_pointer,
@@ -32,8 +35,9 @@ use smithay::{
32
35
33
36
use crate::{
34
37
focus::{KeyboardFocusTarget, PointerFocusTarget},
38
+ grabs::PointerMoveSurfaceGrab,
35
39
state::MyCompositor,
36
- window::PaganWindow,
40
+ window::{PaganWindow, TitlebarButton},
37
41
};
38
42
39
43
impl MyCompositor {
@@ -280,6 +284,11 @@ impl MyCompositor {
280
284
let state = wl_pointer::ButtonState::from(evt.state());
281
285
282
286
if wl_pointer::ButtonState::Pressed == state {
287
+ // SSD: klik w pasek tytułu obsługujemy sami (przeciąganie / przyciski okna).
288
+ // Konsumujemy zdarzenie, żeby nie poszło też do klienta.
289
+ if self.handle_ssd_press(button, serial) {
290
+ return;
291
+ }
283
292
self.update_keyboard_focus(self.pointer_location, serial);
284
293
}
285
294
@@ -296,6 +305,65 @@ impl MyCompositor {
296
305
pointer.frame(self);
297
306
}
298
307
308
+ /// Najwyżej położone okno SSD, którego pasek tytułu zawiera punkt.
309
+ ///
310
+ /// PO CO `rev()`: `space.elements()` idzie od spodu do wierzchu, a my chcemy
311
+ /// trafić w okno leżące na wierzchu.
312
+ fn ssd_hit(&self, point: Point<f64, Logical>) -> Option<(PaganWindow, Point<i32, Logical>)> {
313
+ for window in self.space.elements().rev() {
314
+ let Some(location) = self.space.element_location(window) else {
315
+ continue;
316
+ };
317
+ if window.titlebar_contains(location, point) {
318
+ return Some((window.clone(), location));
319
+ }
320
+ }
321
+ None
322
+ }
323
+
324
+ /// Obsługa wciśnięcia przycisku na pasku tytułu (SSD).
325
+ /// `true` = zdarzenie skonsumowane (nie idzie do klienta).
326
+ fn handle_ssd_press(&mut self, button: u32, serial: Serial) -> bool {
327
+ let point = self.pointer_location;
328
+ let Some((window, location)) = self.ssd_hit(point) else {
329
+ return false;
330
+ };
331
+
332
+ // Klik w pasek też aktywuje okno (jak klik w treść).
333
+ self.raise_and_focus(&window, serial);
334
+
335
+ match window.titlebar_button_at(location, point) {
336
+ Some(TitlebarButton::Close) => self.close_window(&window),
337
+ Some(TitlebarButton::Minimize) => self.minimize_window(&window),
338
+ Some(TitlebarButton::Maximize) => {
339
+ if window.is_maximized() {
340
+ self.unmaximize_window(&window);
341
+ } else {
342
+ self.maximize_window(&window);
343
+ }
344
+ }
345
+ None => {
346
+ // Przeciąganie za pasek — ten sam grab co przy `xdg_toplevel.move`.
347
+ let start_data = PointerGrabStartData {
348
+ focus: None,
349
+ button,
350
+ location: point,
351
+ };
352
+ let grab = PointerMoveSurfaceGrab {
353
+ start_data,
354
+ initial_window_location: location,
355
+ initial_window_size: window.geometry().size,
356
+ window,
357
+ current_snap: None,
358
+ };
359
+ self.snap_preview = None;
360
+ let pointer = self.pointer.clone();
361
+ pointer.set_grab(self, grab, serial, Focus::Clear);
362
+ }
363
+ }
364
+ true
365
+ }
366
+
299
367
/// Scroll. Przekazujemy do klienta pod kursorem (np. przewijanie treści).
300
368
pub(crate) fn on_pointer_axis<B: InputBackend>(&mut self, evt: B::PointerAxisEvent) {
301
369
let horizontal_amount = evt
@@ -25,7 +25,7 @@ use smithay::{
25
25
26
26
use crate::{
27
27
state::{MyCompositor, CASCADE_STEP},
28
- window::PaganWindow,
28
+ window::{PaganWindow, TITLEBAR_HEIGHT},
29
29
};
30
30
31
31
/// Skala odczytana z `PAGAN_SCALE` (np. `1.5`). `None`, gdy nie ustawiono —
@@ -282,10 +282,34 @@ impl MyCompositor {
282
282
.unwrap_or_else(|| Rectangle::from_size((1280, 720).into()));
283
283
284
284
let location = self.next_cascade_in(area);
285
+ let location = self.adjust_for_ssd(&window, location);
285
286
self.space.map_element(window, location, true);
286
287
self.advance_cascade(area);
287
288
}
288
289
290
+ /// Przesuwa pozycję okna tak, aby pasek tytułu (SSD) był widoczny.
291
+ ///
292
+ /// PO CO: w naszym modelu `location` to początek TREŚCI okna, a pasek SSD
293
+ /// rysujemy NAD nią (`location.y - TITLEBAR_HEIGHT`). Bez tej korekty okno
294
+ /// postawione w kaskadzie na (20,20) miałoby pasek poza ekranem.
295
+ pub(crate) fn adjust_for_ssd(
296
+ &self,
297
+ window: &PaganWindow,
298
+ mut location: Point<i32, Logical>,
299
+ ) -> Point<i32, Logical> {
300
+ if window.wants_ssd() {
301
+ let top = self
302
+ .space
303
+ .outputs()
304
+ .next()
305
+ .and_then(|output| self.space.output_geometry(output))
306
+ .map(|area| area.loc.y)
307
+ .unwrap_or(0);
308
+ location.y = location.y.max(top + TITLEBAR_HEIGHT);
309
+ }
310
+ location
311
+ }
312
+
289
313
/// Zwraca prostokąty wszystkich wyjść.
290
314
fn output_areas(&self) -> Vec<Rectangle<i32, Logical>> {
291
315
self.space
@@ -330,20 +330,22 @@ fn preview_program(renderer: &mut GlesRenderer) -> Option<GlesPixelProgram> {
330
330
}
331
331
}
332
332
333
-/// Buduje element „ducha” — zaokrąglony, półprzezroczysty prostokąt z motywu.
334
-fn preview_element(
333
+/// Buduje element zaokrąglonego prostokąta o zadanym kolorze i promieniu.
334
+///
335
+/// Wspólny dla „ducha” doklejania i dekoracji serwerowych (SSD): obie rzeczy to
336
+/// po prostu zaokrąglone, kolorowe kształty rysowane pixel shaderem.
337
+pub fn rounded_rect_element(
335
338
renderer: &mut GlesRenderer,
336
- geometry: Rectangle<i32, Logical>,
339
+ area: Rectangle<i32, Logical>,
340
+ color: Color32F,
341
+ radius: f32,
337
342
) -> Option<PixelShaderElement> {
338
- if geometry.size.w <= 0 || geometry.size.h <= 0 {
343
+ if area.size.w <= 0 || area.size.h <= 0 {
339
344
return None;
340
345
}
341
346
let program = preview_program(renderer)?;
342
- let theme = theme::current();
343
- let color = theme.snap_preview;
344
-
345
347
let uniforms = vec![
346
- Uniform::new("radius", theme.snap_preview_radius),
348
+ Uniform::new("radius", radius),
347
349
Uniform::new("fill_color", (color.r(), color.g(), color.b(), color.a())),
348
350
// ~1 px wygładzenia krawędzi (antyaliasing kształtu).
349
351
Uniform::new("softness", 1.0f32),
@@ -351,7 +353,7 @@ fn preview_element(
351
353
352
354
Some(PixelShaderElement::new(
353
355
program,
354
- geometry,
356
+ area,
355
357
None,
356
358
1.0,
357
359
uniforms,
@@ -359,6 +361,20 @@ fn preview_element(
359
361
))
360
362
}
361
363
364
+/// Buduje element „ducha” — zaokrąglony, półprzezroczysty prostokąt z motywu.
365
+fn preview_element(
366
+ renderer: &mut GlesRenderer,
367
+ geometry: Rectangle<i32, Logical>,
368
+) -> Option<PixelShaderElement> {
369
+ let theme = theme::current();
370
+ rounded_rect_element(
371
+ renderer,
372
+ geometry,
373
+ theme.snap_preview,
374
+ theme.snap_preview_radius,
375
+ )
376
+}
377
+
362
378
/// Kompiluje shadery „na zimno” przy starcie.
363
379
///
364
380
/// PO CO: błąd GLSL ma wylecieć od razu (w logu startowym), a nie dopiero przy
@@ -995,26 +995,30 @@ impl SeatHandler for MyCompositor {
995
995
996
996
impl XdgDecorationHandler for MyCompositor {
997
997
fn new_decoration(&mut self, toplevel: ToplevelSurface) {
998
- // KLUCZOWA DECYZJA: domyślnie wymuszamy tryb CLIENT-SIDE.
999
- // To jest fundament "hybrydowego CSD" — całe dekoracje (pasek tytułu,
1000
- // menu, przyciski) rysuje klient naszego toolkitu, dzięki czemu menu
1001
- // i przyciski są zawsze idealnie wyrównane (rysuje je ten sam kod).
998
+ // DECYZJA: wymuszamy tryb SERVER-SIDE. To odwrócenie wcześniejszego
999
+ // "hybrydowego CSD" — od teraz to KOMPOZYTOR rysuje pasek tytułu
1000
+ // (spójnie dla WSZYSTKICH aplikacji: GTK przestaje rysować swoje CSD,
1001
+ // a X11 i tak nie ma dekoracji, więc dostaje nasze).
1002
+ //
1003
+ // Nasz własny toolkit nie tworzy obiektu dekoracji, więc tego nie
1004
+ // negocjuje i zachowuje swoje CSD — dlatego krok 2 (toolkit) musi
1005
+ // nastąpić, żeby nie było podwójnych pasków.
1002
1006
toplevel.with_pending_state(|state| {
1003
- state.decoration_mode = Some(DecorationMode::ClientSide);
1007
+ state.decoration_mode = Some(DecorationMode::ServerSide);
1004
1008
});
1005
1009
}
1006
1010
1007
- fn request_mode(&mut self, toplevel: ToplevelSurface, mode: DecorationMode) {
1008
- // Nie zmuszamy na siłę: jeśli klient wprost prosi o tryb, respektujemy go.
1011
+ fn request_mode(&mut self, toplevel: ToplevelSurface, _mode: DecorationMode) {
1012
+ // Ignorujemy preferencję klienta — dekoracje rysujemy my.
1009
1013
toplevel.with_pending_state(|state| {
1010
- state.decoration_mode = Some(mode);
1014
+ state.decoration_mode = Some(DecorationMode::ServerSide);
1011
1015
});
1012
1016
}
1013
1017
1014
1018
fn unset_mode(&mut self, toplevel: ToplevelSurface) {
1015
- // Klient nie ma preferencji — wracamy do naszego domyślnego CSD.
1019
+ // Brak preferencji klienta = nadal nasz pasek serwerowy.
1016
1020
toplevel.with_pending_state(|state| {
1017
- state.decoration_mode = Some(DecorationMode::ClientSide);
1021
+ state.decoration_mode = Some(DecorationMode::ServerSide);
1018
1022
});
1019
1023
}
1020
1024
}
@@ -30,10 +30,17 @@ use smithay::{
30
30
element::PixelShaderElement, GlesPixelProgram, GlesRenderer, Uniform, UniformName,
31
31
UniformType,
32
32
},
33
+ Color32F,
33
34
},
34
35
desktop::{space::SpaceElement, Window, WindowSurface},
35
36
output::Output,
36
- reexports::wayland_server::protocol::wl_surface::WlSurface,
37
+ reexports::{
38
+ wayland_protocols::xdg::{
39
+ decoration::zv1::server::zxdg_toplevel_decoration_v1::Mode as DecorationMode,
40
+ shell::server::xdg_toplevel,
41
+ },
42
+ wayland_server::protocol::wl_surface::WlSurface,
43
+ },
37
44
render_elements,
38
45
utils::{user_data::UserDataMap, IsAlive, Logical, Physical, Point, Rectangle, Scale, Size},
39
46
wayland::{
@@ -41,7 +48,31 @@ use smithay::{
41
48
},
42
49
};
43
50
44
-use crate::{shaders::SHADOW_FRAG, theme};
51
+use crate::{render, shaders::SHADOW_FRAG, theme};
52
+
53
+// --- Dekoracje serwerowe (SSD) ------------------------------------------------
54
+/// Wysokość paska tytułu rysowanego przez kompozytor.
55
+pub const TITLEBAR_HEIGHT: i32 = 40;
56
+/// Promień okrągłych przycisków okna.
57
+const BUTTON_RADIUS: i32 = 6;
58
+/// Odległość środka skrajnego (zamykającego) przycisku od prawej krawędzi.
59
+const BUTTON_MARGIN: i32 = 22;
60
+/// Odstęp między środkami przycisków.
61
+const BUTTON_SPACING: i32 = 26;
62
+/// Kolor belki paska (ciemny, spójny z panelem/motywem).
63
+const TITLEBAR_COLOR: Color32F = Color32F::new(0.090, 0.098, 0.125, 1.0);
64
+/// Kolory przycisków — jak w toolkicie (minimalizuj/maksymalizuj/zamknij).
65
+const BUTTON_MINIMIZE: Color32F = Color32F::new(0.90, 0.72, 0.28, 1.0);
66
+const BUTTON_MAXIMIZE: Color32F = Color32F::new(0.32, 0.72, 0.40, 1.0);
67
+const BUTTON_CLOSE: Color32F = Color32F::new(0.82, 0.30, 0.30, 1.0);
68
+
69
+/// Który przycisk paska tytułu trafiono.
70
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71
+pub enum TitlebarButton {
72
+ Minimize,
73
+ Maximize,
74
+ Close,
75
+}
45
76
46
77
// Parametry cienia (margines, rozmycie, przesunięcie, siła, promień rogów)
47
78
// pochodzą z motywu (`theme::Theme`) — patrz `~/.config/pagan/theme.conf`.
@@ -132,6 +163,105 @@ impl PaganWindow {
132
163
pub fn user_data(&self) -> &UserDataMap {
133
164
self.0.user_data()
134
165
}
166
+
167
+ /// Czy to okno ma dostać nasz pasek tytułu (dekoracja serwerowa)?
168
+ ///
169
+ /// PO CO to rozróżnienie: nasz toolkit nie tworzy obiektu dekoracji, więc
170
+ /// zachowuje własne CSD — gdybyśmy dołożyli mu SSD, powstałyby dwa paski.
171
+ /// Dlatego SSD dajemy tylko tam, gdzie klient sam się nie dekoruje:
172
+ /// X11 (zawsze) oraz Wayland, który wynegocjował tryb serwerowy.
173
+ pub fn wants_ssd(&self) -> bool {
174
+ if self.x11_surface().is_some() {
175
+ return true;
176
+ }
177
+ self.toplevel()
178
+ .map(|toplevel| {
179
+ let mut server_side = false;
180
+ toplevel.with_pending_state(|state| {
181
+ server_side = state.decoration_mode == Some(DecorationMode::ServerSide);
182
+ });
183
+ server_side
184
+ })
185
+ .unwrap_or(false)
186
+ }
187
+
188
+ /// Czy okno jest zmaksymalizowane (Wayland: stan xdg, X11: `_NET_WM_STATE`).
189
+ pub fn is_maximized(&self) -> bool {
190
+ if let Some(toplevel) = self.toplevel() {
191
+ let mut maximized = false;
192
+ toplevel.with_pending_state(|state| {
193
+ maximized = state.states.contains(xdg_toplevel::State::Maximized);
194
+ });
195
+ return maximized;
196
+ }
197
+ if let Some(x11) = self.x11_surface() {
198
+ return x11.is_maximized();
199
+ }
200
+ false
201
+ }
202
+
203
+ /// Prostokąt paska tytułu dla okna postawionego w `location` (układ logiczny).
204
+ pub fn titlebar_rect(&self, location: Point<i32, Logical>) -> Rectangle<i32, Logical> {
205
+ Rectangle::new(
206
+ Point::from((location.x, location.y - TITLEBAR_HEIGHT)),
207
+ Size::from((self.geometry().size.w, TITLEBAR_HEIGHT)),
208
+ )
209
+ }
210
+
211
+ /// Środki i kolory trzech przycisków paska (od prawej: zamknij, maks., min.).
212
+ fn titlebar_buttons(
213
+ &self,
214
+ bar: Rectangle<i32, Logical>,
215
+ ) -> [(TitlebarButton, Point<i32, Logical>, Color32F); 3] {
216
+ let center_y = bar.loc.y + TITLEBAR_HEIGHT / 2;
217
+ let right = bar.loc.x + bar.size.w;
218
+ [
219
+ (
220
+ TitlebarButton::Close,
221
+ Point::from((right - BUTTON_MARGIN, center_y)),
222
+ BUTTON_CLOSE,
223
+ ),
224
+ (
225
+ TitlebarButton::Maximize,
226
+ Point::from((right - BUTTON_MARGIN - BUTTON_SPACING, center_y)),
227
+ BUTTON_MAXIMIZE,
228
+ ),
229
+ (
230
+ TitlebarButton::Minimize,
231
+ Point::from((right - BUTTON_MARGIN - BUTTON_SPACING * 2, center_y)),
232
+ BUTTON_MINIMIZE,
233
+ ),
234
+ ]
235
+ }
236
+
237
+ /// Czy punkt (w układzie przestrzeni) leży na pasku tytułu tego okna.
238
+ pub fn titlebar_contains(
239
+ &self,
240
+ location: Point<i32, Logical>,
241
+ point: Point<f64, Logical>,
242
+ ) -> bool {
243
+ self.wants_ssd() && self.titlebar_rect(location).to_f64().contains(point)
244
+ }
245
+
246
+ /// Który przycisk paska trafiono (jeśli którykolwiek).
247
+ pub fn titlebar_button_at(
248
+ &self,
249
+ location: Point<i32, Logical>,
250
+ point: Point<f64, Logical>,
251
+ ) -> Option<TitlebarButton> {
252
+ if !self.wants_ssd() {
253
+ return None;
254
+ }
255
+ let bar = self.titlebar_rect(location);
256
+ for (button, center, _) in self.titlebar_buttons(bar) {
257
+ let dx = point.x - center.x as f64;
258
+ let dy = point.y - center.y as f64;
259
+ if dx * dx + dy * dy <= ((BUTTON_RADIUS + 3) as f64).powi(2) {
260
+ return Some(button);
261
+ }
262
+ }
263
+ None
264
+ }
135
265
}
136
266
137
267
impl IsAlive for PaganWindow {
@@ -152,7 +282,16 @@ impl SpaceElement for PaganWindow {
152
282
SpaceElement::geometry(&self.0)
153
283
}
154
284
fn bbox(&self) -> Rectangle<i32, Logical> {
155
- SpaceElement::bbox(&self.0)
285
+ let bbox = SpaceElement::bbox(&self.0);
286
+ if self.wants_ssd() {
287
+ // Rama = pasek + treść; bbox musi go obejmować (rejon uszkodzeń, wyjścia).
288
+ Rectangle::new(
289
+ Point::from((bbox.loc.x, bbox.loc.y - TITLEBAR_HEIGHT)),
290
+ Size::from((bbox.size.w, bbox.size.h + TITLEBAR_HEIGHT)),
291
+ )
292
+ } else {
293
+ bbox
294
+ }
156
295
}
157
296
fn is_in_input_region(&self, point: &Point<f64, Logical>) -> bool {
158
297
SpaceElement::is_in_input_region(&self.0, point)
@@ -182,14 +321,17 @@ impl SpaceElement for PaganWindow {
182
321
render_elements! {
183
322
pub WindowRenderElement<=GlesRenderer>;
184
323
Window=WaylandSurfaceRenderElement<GlesRenderer>,
185
- Shadow=PixelShaderElement,
324
+ // Element rysowany pixel shaderem: cień okna ORAZ pasek tytułu/przyciski SSD.
325
+ // (Jedna wariant, bo makro generuje `From` per typ — dwa warianty tego samego
326
+ // typu kolidowałyby.)
327
+ Pixel=PixelShaderElement,
186
328
}
187
329
188
330
impl std::fmt::Debug for WindowRenderElement {
189
331
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
190
332
match self {
191
333
Self::Window(arg0) => f.debug_tuple("Window").field(arg0).finish(),
192
- Self::Shadow(arg0) => f.debug_tuple("Shadow").field(arg0).finish(),
334
+ Self::Pixel(arg0) => f.debug_tuple("Pixel").field(arg0).finish(),
193
335
Self::_GenericCatcher(arg0) => f.debug_tuple("_GenericCatcher").field(arg0).finish(),
194
336
}
195
337
}
@@ -212,13 +354,51 @@ impl AsRenderElements<GlesRenderer> for PaganWindow {
212
354
// Dlatego najpierw treść okna (na wierzchu), a cień dopiero na końcu —
213
355
// czyli POD oknem.
214
356
215
- // 1) Treść okna (klient narysował już pasek tytułu, menu i przyciski).
357
+ // 1) Treść okna (klient narysował już swoje wnętrze).
216
358
elements.extend(AsRenderElements::<GlesRenderer>::render_elements::<
217
359
WindowRenderElement,
218
360
>(&self.0, renderer, location, scale, alpha));
219
361
220
- // 2) Cień POD oknem (ta sama grupa elementów = wspólny Z-index z oknem).
221
- if let Some(shadow) = shadow_element(renderer, location, scale, self.geometry().size) {
362
+ // 1b) SSD: pasek tytułu + okrągłe przyciski (rysuje kompozytor).
363
+ if self.wants_ssd() {
364
+ let logical_loc = location.to_f64().to_logical(scale).to_i32_round::<i32>();
365
+ let bar = Rectangle::new(
366
+ Point::from((logical_loc.x, logical_loc.y - TITLEBAR_HEIGHT)),
367
+ Size::from((self.geometry().size.w, TITLEBAR_HEIGHT)),
368
+ );
369
+ // Kolejność: NAJPIERW przyciski, POTEM belka — bo lista jest
370
+ // front-to-back (mniejszy indeks = bliżej przodu), a belka i przyciski
371
+ // nachodzą na siebie. Odwrotna kolejność zasłoniłaby przyciski.
372
+ for (_, center, color) in self.titlebar_buttons(bar) {
373
+ let area = Rectangle::new(
374
+ Point::from((center.x - BUTTON_RADIUS, center.y - BUTTON_RADIUS)),
375
+ Size::from((BUTTON_RADIUS * 2, BUTTON_RADIUS * 2)),
376
+ );
377
+ if let Some(element) =
378
+ render::rounded_rect_element(renderer, area, color, BUTTON_RADIUS as f32)
379
+ {
380
+ elements.push(WindowRenderElement::Pixel(element));
381
+ }
382
+ }
383
+ // Belka (delikatnie zaokrąglona — górne rogi okna).
384
+ if let Some(element) = render::rounded_rect_element(renderer, bar, TITLEBAR_COLOR, 10.0)
385
+ {
386
+ elements.push(WindowRenderElement::Pixel(element));
387
+ }
388
+ tracing::info!(count = elements.len(), "DIAG: SSD gotowe");
389
+ }
390
+
391
+ // 2) Cień POD RAMĄ (pasek + treść) — ta sama grupa elementów = wspólny Z-order.
392
+ let (shadow_location, shadow_size) = if self.wants_ssd() {
393
+ let geometry = self.geometry().size;
394
+ (
395
+ Point::from((location.x, location.y - TITLEBAR_HEIGHT)),
396
+ Size::from((geometry.w, geometry.h + TITLEBAR_HEIGHT)),
397
+ )
398
+ } else {
399
+ (location, self.geometry().size)
400
+ };
401
+ if let Some(shadow) = shadow_element(renderer, shadow_location, scale, shadow_size) {
222
402
elements.push(shadow);
223
403
}
224
404
@@ -325,7 +505,7 @@ fn shadow_element(
325
505
),
326
506
];
327
507
328
- Some(WindowRenderElement::Shadow(PixelShaderElement::new(
508
+ Some(WindowRenderElement::Pixel(PixelShaderElement::new(
329
509
program,
330
510
area,
331
511
None,
@@ -227,7 +227,9 @@ impl XwmHandler for MyCompositor {
227
227
_above: Option<X11Window>,
228
228
) {
229
229
if let Some(element) = self.element_for_x11(&window) {
230
- self.space.map_element(element, geometry.loc, false);
230
+ // X11 podaje pozycję TREŚCI; dla SSD pilnujemy, żeby pasek był na ekranie.
231
+ let location = self.adjust_for_ssd(&element, geometry.loc);
232
+ self.space.map_element(element, location, false);
231
233
}
232
234
}
233
235