← PaganDE

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