← PaganDE

Commit b095cdf

8
plików
+205
dodanych
-37
usuniętych
@@ -216,6 +216,7 @@ impl PointerGrab<MyCompositor> for PointerMoveSurfaceGrab {
216 216 // Martwe okno (klient je zamknął) — kończymy grab, żeby nie ruszać
217 217 // powierzchni, której już nie ma.
218 218 if !self.window.alive() {
219 + data.snap_preview = None;
219 220 handle.unset_grab(self, data, event.serial, event.time, true);
220 221 return;
221 222 }
@@ -223,7 +224,9 @@ impl PointerGrab<MyCompositor> for PointerMoveSurfaceGrab {
223 224 let delta = event.location - self.start_data.location;
224 225 let new_location = self.initial_window_location.to_f64() + delta;
225 226
226 - // Czy kursor wjechał w strefę doklejania na którymś z wyjść?
227 + // Czy kursor jest w strefie doklejania? Jeśli tak, pokazujemy DUCHA
228 + // (podgląd geometrii) — ale okna jeszcze NIE zmieniamy rozmiarem ani
229 + // pozycją docelową. Snap zatwierdzamy dopiero przy puszczeniu przycisku.
227 230 let target = Self::output_under(data, event.location).and_then(|geometry| {
228 231 snap_zone(event.location, geometry, data.snap_config)
229 232 .map(|zone| (zone, snap_geometry(zone, geometry)))
@@ -231,28 +234,18 @@ impl PointerGrab<MyCompositor> for PointerMoveSurfaceGrab {
231 234
232 235 match target {
233 236 Some((zone, geometry)) => {
234 - // Wchodzimy w strefę LUB przechodzimy między strefami (np. róg→krawędź).
235 - if self.current_snap != Some(zone) {
236 - self.current_snap = Some(zone);
237 - // Geometrię (pozycję i rozmiar) przekazujemy klientowi.
238 - self.send_geometry(geometry, Some(zone));
239 - data.space
240 - .map_element(self.window.clone(), geometry.loc, false);
241 - }
237 + self.current_snap = Some(zone);
238 + data.snap_preview = Some(geometry);
242 239 }
243 240 None => {
244 - // Wyjście ze strefy: przywróć rozmiar sprzed doklejenia i podążaj
245 - // z powrotem za kursorem.
246 - if self.current_snap.take().is_some() {
247 - self.send_geometry(
248 - Rectangle::new(new_location.to_i32_round(), self.initial_window_size),
249 - None,
250 - );
251 - }
252 - data.space
253 - .map_element(self.window.clone(), new_location.to_i32_round(), false);
241 + self.current_snap = None;
242 + data.snap_preview = None;
254 243 }
255 244 }
245 +
246 + // Okno przez cały czas podąża za kursorem, w rozmiarze z początku ruchu.
247 + data.space
248 + .map_element(self.window.clone(), new_location.to_i32_round(), false);
256 249 }
257 250
258 251 fn relative_motion(
@@ -273,6 +266,20 @@ impl PointerGrab<MyCompositor> for PointerMoveSurfaceGrab {
273 266 ) {
274 267 handle.button(data, event);
275 268 if handle.current_pressed().is_empty() {
269 + // Puszczono wszystkie przyciski. DOPIERO TERAZ zatwierdzamy snap:
270 + // jeśli kursor stał w strefie, wysyłamy klientowi docelową geometrię
271 + // i ustawiamy okno; w przeciwnym razie okno zostaje tam, gdzie je
272 + // przeciągnęliśmy.
273 + if let Some(zone) = self.current_snap.take() {
274 + if let Some(geometry) = Self::output_under(data, data.pointer_location)
275 + .map(|output| snap_geometry(zone, output))
276 + {
277 + self.send_geometry(geometry, Some(zone));
278 + data.space
279 + .map_element(self.window.clone(), geometry.loc, false);
280 + }
281 + }
282 + data.snap_preview = None;
276 283 // Puszczono wszystkie przyciski — kończymy przeciąganie.
277 284 handle.unset_grab(self, data, event.serial, event.time, true);
278 285 }
@@ -364,7 +371,10 @@ impl PointerGrab<MyCompositor> for PointerMoveSurfaceGrab {
364 371 &self.start_data
365 372 }
366 373
367 - fn unset(&mut self, _data: &mut MyCompositor) {}
374 + fn unset(&mut self, data: &mut MyCompositor) {
375 + // Grab bywa zdejmowany z różnych powodów — zawsze chowamy ducha snapu.
376 + data.snap_preview = None;
377 + }
368 378 }
369 379
370 380 /// Grab zmiany rozmiaru okna za krawędź.
@@ -8,7 +8,7 @@
8 8 //! z konkretnym rendererem. Używamy `GlesRenderer` w obu backendach (winit, udev),
9 9 //! więc nie tracimy na tym nic, a zyskujemy prostsze typy.
10 10
11 -use std::{fmt, time::Duration};
11 +use std::{collections::HashMap, fmt, sync::Mutex, sync::OnceLock, time::Duration};
12 12
13 13 use smithay::{
14 14 backend::renderer::{
@@ -19,7 +19,10 @@ use smithay::{
19 19 surface::{render_elements_from_surface_tree, WaylandSurfaceRenderElement},
20 20 Id, Kind,
21 21 },
22 - gles::{GlesError, GlesRenderer, GlesTarget},
22 + gles::{
23 + element::PixelShaderElement, GlesError, GlesPixelProgram, GlesRenderer, GlesTarget,
24 + Uniform, UniformName, UniformType,
25 + },
23 26 utils::CommitCounter,
24 27 Color32F,
25 28 },
@@ -31,6 +34,7 @@ use smithay::{
31 34 };
32 35
33 36 use crate::{
37 + shaders::ROUNDED_CORNERS_FRAG,
34 38 state::DndIcon,
35 39 theme,
36 40 window::{PaganWindow, WindowRenderElement},
@@ -52,6 +56,7 @@ render_elements! {
52 56 Surface=WaylandSurfaceRenderElement<GlesRenderer>,
53 57 Memory=MemoryRenderBufferRenderElement<GlesRenderer>,
54 58 Pointer=SolidColorRenderElement,
59 + Preview=PixelShaderElement,
55 60 }
56 61
57 62 impl fmt::Debug for CustomRenderElements {
@@ -60,6 +65,7 @@ impl fmt::Debug for CustomRenderElements {
60 65 Self::Surface(arg0) => f.debug_tuple("Surface").field(arg0).finish(),
61 66 Self::Memory(arg0) => f.debug_tuple("Memory").field(arg0).finish(),
62 67 Self::Pointer(arg0) => f.debug_tuple("Pointer").field(arg0).finish(),
68 + Self::Preview(arg0) => f.debug_tuple("Preview").field(arg0).finish(),
63 69 Self::_GenericCatcher(arg0) => f.debug_tuple("_GenericCatcher").field(arg0).finish(),
64 70 }
65 71 }
@@ -208,12 +214,13 @@ impl PointerElement {
208 214 }
209 215 }
210 216
211 -/// Składa elementy klatki: okna z przestrzeni + nakładki (kursor, ikona DnD).
217 +/// Składa elementy klatki: okna z przestrzeni + nakładki (kursor, DnD, duch snapu).
212 218 pub fn output_elements(
213 219 space: &Space<PaganWindow>,
214 220 output: &Output,
215 221 pointer: Option<&mut PointerElement>,
216 222 dnd: Option<&DndIcon>,
223 + preview: Option<Rectangle<i32, Logical>>,
217 224 pointer_location: Point<f64, Logical>,
218 225 renderer: &mut GlesRenderer,
219 226 ) -> (Vec<OutputRenderElements>, Color32F) {
@@ -264,26 +271,129 @@ pub fn output_elements(
264 271 );
265 272 }
266 273
267 - // 3) Zawartość przestrzeni (okna razem z ich cieniami) — na spodzie.
274 + // 3) „Duch” doklejania (półprzezroczysty, zaokrąglony podgląd geometrii) —
275 + // nad oknami, ale pod kursorem. Pokazujemy go, zanim użytkownik puści
276 + // przycisk, żeby wiedział, gdzie okno wyląduje.
277 + if let Some(geometry) = preview {
278 + if let Some(element) = preview_element(renderer, geometry) {
279 + elements.push(OutputRenderElements::Custom(CustomRenderElements::Preview(
280 + element,
281 + )));
282 + }
283 + }
284 +
285 + // 4) Zawartość przestrzeni (okna razem z ich cieniami) — na spodzie.
268 286 elements.extend(space_elements.into_iter().map(OutputRenderElements::Space));
269 287
270 288 (elements, theme::current().background)
271 289 }
272 290
291 +/// Cache programów „ducha” (shader zaokrągleń), kluczowany kontekstem EGL.
292 +///
293 +/// PO CO klucz po kontekście: program GL żyje w konkretnym kontekście — ten sam
294 +/// powód co przy shaderze cienia (patrz `window.rs`).
295 +fn preview_programs() -> &'static Mutex<HashMap<usize, GlesPixelProgram>> {
296 + static CACHE: OnceLock<Mutex<HashMap<usize, GlesPixelProgram>>> = OnceLock::new();
297 + CACHE.get_or_init(|| Mutex::new(HashMap::new()))
298 +}
299 +
300 +/// Kompiluje (raz na kontekst) i zwraca program zaokrąglonego prostokąta.
301 +fn preview_program(renderer: &mut GlesRenderer) -> Option<GlesPixelProgram> {
302 + let key = renderer.egl_context().get_context_handle() as usize;
303 +
304 + if let Some(program) = preview_programs()
305 + .lock()
306 + .expect("cache shaderów zatruty")
307 + .get(&key)
308 + {
309 + return Some(program.clone());
310 + }
311 +
312 + let uniforms = [
313 + UniformName::new("radius", UniformType::_1f),
314 + UniformName::new("fill_color", UniformType::_4f),
315 + UniformName::new("softness", UniformType::_1f),
316 + ];
317 +
318 + match renderer.compile_custom_pixel_shader(ROUNDED_CORNERS_FRAG, &uniforms) {
319 + Ok(program) => {
320 + preview_programs()
321 + .lock()
322 + .expect("cache shaderów zatruty")
323 + .insert(key, program.clone());
324 + Some(program)
325 + }
326 + Err(err) => {
327 + tracing::error!(?err, "nie udało się skompilować shadera ducha snapu");
328 + None
329 + }
330 + }
331 +}
332 +
333 +/// Buduje element „ducha” — zaokrąglony, półprzezroczysty prostokąt z motywu.
334 +fn preview_element(
335 + renderer: &mut GlesRenderer,
336 + geometry: Rectangle<i32, Logical>,
337 +) -> Option<PixelShaderElement> {
338 + if geometry.size.w <= 0 || geometry.size.h <= 0 {
339 + return None;
340 + }
341 + let program = preview_program(renderer)?;
342 + let theme = theme::current();
343 + let color = theme.snap_preview;
344 +
345 + let uniforms = vec![
346 + Uniform::new("radius", theme.snap_preview_radius),
347 + Uniform::new("fill_color", (color.r(), color.g(), color.b(), color.a())),
348 + // ~1 px wygładzenia krawędzi (antyaliasing kształtu).
349 + Uniform::new("softness", 1.0f32),
350 + ];
351 +
352 + Some(PixelShaderElement::new(
353 + program,
354 + geometry,
355 + None,
356 + 1.0,
357 + uniforms,
358 + Kind::Unspecified,
359 + ))
360 +}
361 +
362 +/// Kompiluje shadery „na zimno” przy starcie.
363 +///
364 +/// PO CO: błąd GLSL ma wylecieć od razu (w logu startowym), a nie dopiero przy
365 +/// pierwszym przeciąganiu okna; przy okazji pierwsza klatka nie haczy na kompilacji.
366 +pub fn warm_up_shaders(renderer: &mut GlesRenderer) {
367 + match preview_program(renderer) {
368 + Some(_) => tracing::info!("shader ducha snapu (rounded_corners) skompilowany"),
369 + None => tracing::warn!(
370 + "shader ducha snapu NIE skompilował się — duch doklejania nie będzie rysowany"
371 + ),
372 + }
373 +}
374 +
273 375 /// Renderuje jedną klatkę wyjścia i zwraca wynik z listą uszkodzonych regionów.
274 376 pub fn render_output<'d>(
275 377 output: &Output,
276 378 space: &Space<PaganWindow>,
277 379 pointer: Option<&mut PointerElement>,
278 380 dnd: Option<&DndIcon>,
381 + preview: Option<Rectangle<i32, Logical>>,
279 382 pointer_location: Point<f64, Logical>,
280 383 renderer: &mut GlesRenderer,
281 384 framebuffer: &mut GlesTarget<'_>,
282 385 damage_tracker: &'d mut OutputDamageTracker,
283 386 age: usize,
284 387 ) -> Result<RenderOutputResult<'d>, OutputDamageTrackerError<GlesError>> {
285 - let (elements, clear_color) =
286 - output_elements(space, output, pointer, dnd, pointer_location, renderer);
388 + let (elements, clear_color) = output_elements(
389 + space,
390 + output,
391 + pointer,
392 + dnd,
393 + preview,
394 + pointer_location,
395 + renderer,
396 + );
287 397 damage_tracker.render_output(renderer, framebuffer, age, &elements, clear_color)
288 398 }
289 399
@@ -290,12 +290,14 @@ fn capture(
290 290 None
291 291 };
292 292
293 - // Ikony DnD nie rysujemy w zrzutach — to stan przejściowy interakcji.
293 + // Ikony DnD i ducha snapu nie rysujemy w zrzutach — to stan przejściowy interakcji.
294 + // Ikony DnD i ducha snapu nie rysujemy w zrzutach — to stan przejściowy interakcji.
294 295 let (elements, clear_color) = render::output_elements(
295 296 space,
296 297 output,
297 298 overlay_pointer.as_mut(),
298 299 None,
300 + None,
299 301 pointer_location,
300 302 renderer,
301 303 );
@@ -18,7 +18,10 @@
18 18 // element tła/obramowania o poprawnym, zaokrąglonym konturze.
19 19 //
20 20 // Konwencja Smithaya: bez `#version`; dostępne v_coords, size, alpha.
21 -// Dodatkowo: radius (promień zaokrąglenia), fill_color (rgb maski), softness.
21 +// Dodatkowo: radius (promień zaokrąglenia), fill_color (RGBA, z alfą!), softness.
22 +//
23 +// PO CO `fill_color` jest `vec4`: przezroczystość „ducha” doklejania musi być
24 +// częścią koloru, bo wbudowany `alpha` renderera to zawsze 1.0.
22 25 //
23 26 // `precision` musi być zadeklarowane samodzielnie — renderer dokleja tylko
24 27 // `#version 100`, a GLSL ES nie ma domyślnej precyzji dla `float`.
@@ -30,7 +33,7 @@ uniform vec2 size;
30 33 uniform float alpha;
31 34
32 35 uniform float radius;
33 -uniform vec3 fill_color;
36 +uniform vec4 fill_color;
34 37 uniform float softness;
35 38
36 39 // Odległość ze znakiem do zaokrąglonego prostokąta (ujemna = wnętrze).
@@ -47,5 +50,5 @@ void main() {
47 50 // smoothstep daje ~1px antyaliasing na krawędzi kształtu.
48 51 float mask = 1.0 - smoothstep(-softness, softness, dist);
49 52
50 - gl_FragColor = vec4(fill_color * mask, mask * alpha);
53 + gl_FragColor = vec4(fill_color.rgb * mask, fill_color.a * mask * alpha);
51 54 }
@@ -105,7 +105,7 @@ use smithay::{
105 105 wayland::{
106 106 xwayland_keyboard_grab::XWaylandKeyboardGrabState, xwayland_shell::XWaylandShellState,
107 107 },
108 - xwayland::X11Wm,
108 + xwayland::{X11Wm, XWaylandClientData},
109 109 };
110 110
111 111 use crate::{
@@ -209,6 +209,12 @@ pub struct MyCompositor {
209 209 /// Stan protokołu `wlr-foreign-toplevel-management` (lista okien dla paneli).
210 210 pub foreign_toplevel: ForeignToplevelState,
211 211
212 + /// Podgląd („duch”) geometrii docelowej przy doklejaniu okna.
213 + ///
214 + /// PO CO: pokazujemy, gdzie okno wyląduje, ZANIM użytkownik puści przycisk —
215 + /// sam snap stosujemy dopiero przy puszczeniu (`grabs.rs`).
216 + pub snap_preview: Option<Rectangle<i32, Logical>>,
217 +
212 218 // --- dmabuf + layer-shell ---
213 219 /// Delegat protokołu `linux-dmabuf` (bufory GPU).
214 220 pub dmabuf_state: DmabufState,
@@ -371,6 +377,7 @@ impl MyCompositor {
371 377 dnd_icon: None,
372 378 minimized: Vec::new(),
373 379 foreign_toplevel: ForeignToplevelState::new(),
380 + snap_preview: None,
374 381 dmabuf_state: DmabufState::new(),
375 382 dmabuf_global: None,
376 383 layer_shell_state,
@@ -840,10 +847,16 @@ impl CompositorHandler for MyCompositor {
840 847 }
841 848
842 849 fn client_compositor_state<'a>(&self, client: &'a Client) -> &'a CompositorClientState {
843 - &client
844 - .get_data::<ClientState>()
845 - .expect("client bez ClientState — patrz init()")
846 - .compositor_state
850 + // Klient XWayland ma WŁASNY typ danych (tworzy go Smithay przy `XWayland::spawn`)
851 + // i też potrzebuje stanu kompozytora — bez tego panikowalibyśmy przy pierwszym
852 + // bindowaniu `wl_output` przez XWayland (a to psuje całą sesję).
853 + if let Some(state) = client.get_data::<XWaylandClientData>() {
854 + return &state.compositor_state;
855 + }
856 + if let Some(state) = client.get_data::<ClientState>() {
857 + return &state.compositor_state;
858 + }
859 + panic!("nieznany typ danych klienta — patrz init()")
847 860 }
848 861
849 862 fn new_surface(&mut self, _surface: &WlSurface) {
@@ -54,6 +54,10 @@ pub struct Theme {
54 54 pub cursor_fill: Color32F,
55 55 /// Kolor obwódki awaryjnego kursora.
56 56 pub cursor_border: Color32F,
57 + /// Kolor „ducha” doklejania okna (z alfą — półprzezroczysty podgląd).
58 + pub snap_preview: Color32F,
59 + /// Promień zaokrąglenia „ducha” (px).
60 + pub snap_preview_radius: f32,
57 61 }
58 62
59 63 impl Default for Theme {
@@ -69,6 +73,8 @@ impl Default for Theme {
69 73 shadow_radius: 12.0,
70 74 cursor_fill: hex(0xf2, 0xf2, 0xf7),
71 75 cursor_border: hex(0x14, 0x14, 0x1a),
76 + snap_preview: rgba(0x4e, 0xa1, 0xff, 0x66),
77 + snap_preview_radius: 12.0,
72 78 }
73 79 }
74 80 }
@@ -78,6 +84,16 @@ const fn hex(r: u8, g: u8, b: u8) -> Color32F {
78 84 Color32F::new(r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, 1.0)
79 85 }
80 86
87 +/// Kolor z kanałem alfa — spójny z zapisem `#RRGGBBAA` w pliku.
88 +const fn rgba(r: u8, g: u8, b: u8, a: u8) -> Color32F {
89 + Color32F::new(
90 + r as f32 / 255.0,
91 + g as f32 / 255.0,
92 + b as f32 / 255.0,
93 + a as f32 / 255.0,
94 + )
95 +}
96 +
81 97 impl Theme {
82 98 /// Parsuje motyw z tekstu. Zwraca motyw i listę ostrzeżeń (nie panikujemy).
83 99 ///
@@ -120,11 +136,13 @@ impl Theme {
120 136 "shadow_radius" => self.shadow_radius = parse_float(value)?.max(0.0),
121 137 "cursor_fill" => self.cursor_fill = parse_color(value).ok_or_else(bad_color)?,
122 138 "cursor_border" => self.cursor_border = parse_color(value).ok_or_else(bad_color)?,
139 + "snap_preview" => self.snap_preview = parse_color(value).ok_or_else(bad_color)?,
140 + "snap_preview_radius" => self.snap_preview_radius = parse_float(value)?.max(0.0),
123 141 other => {
124 142 return Err(format!(
125 143 "nieznany klucz '{other}' (dostępne: background, shadow_color, \
126 144 shadow_opacity, shadow_blur, shadow_offset_y, shadow_radius, \
127 - cursor_fill, cursor_border)"
145 + cursor_fill, cursor_border, snap_preview, snap_preview_radius)"
128 146 ));
129 147 }
130 148 }
@@ -198,6 +216,11 @@ shadow_radius = 12
198 216 # Awaryjny kursor (używany tylko, gdy brak motywu XCursor).
199 217 cursor_fill = #f2f2f7
200 218 cursor_border = #14141a
219 +
220 +# „Duch” doklejania okna (podgląd geometrii, gdy przeciągasz okno do krawędzi/rogu).
221 +# Ostatnia para cyfr to przezroczystość (tu ~40%).
222 +snap_preview = #4ea1ff66
223 +snap_preview_radius = 12
201 224 ";
202 225
203 226 /// Globalny, współdzielony motyw (ładowany leniwie).
@@ -722,6 +722,7 @@ impl MyCompositor {
722 722 &output,
723 723 Some(&mut pointer_element),
724 724 self.dnd_icon.as_ref(),
725 + self.snap_preview,
725 726 pointer_location,
726 727 gles,
727 728 );
@@ -81,6 +81,10 @@ pub fn run_winit() -> anyhow::Result<()> {
81 81 .update_formats(backend.renderer().shm_formats());
82 82 state.enable_dmabuf(backend.renderer().dmabuf_formats());
83 83
84 + // Skompiluj shadery własne już teraz — błąd GLSL ma być widoczny w logu
85 + // startowym, a nie dopiero przy pierwszym przeciąganiu okna.
86 + render::warm_up_shaders(backend.renderer());
87 +
84 88 // XWayland (aplikacje X11). Wołane po `init`, bo wstawia źródło do pętli.
85 89 state.start_xwayland();
86 90
@@ -132,8 +136,9 @@ pub fn run_winit() -> anyhow::Result<()> {
132 136
133 137 let age = backend.buffer_age().unwrap_or(0);
134 138 let space = &state.space;
135 - // Ikona DnD i pozycja kursora są tylko czytane w tym bloku.
139 + // Ikona DnD, duch snapu i pozycja kursora są tylko czytane w tym bloku.
136 140 let dnd = state.dnd_icon.as_ref();
141 + let preview = state.snap_preview;
137 142 let pointer_location = state.pointer_location;
138 143
139 144 let render_res = backend.bind().and_then(|(renderer, mut framebuffer)| {
@@ -142,6 +147,7 @@ pub fn run_winit() -> anyhow::Result<()> {
142 147 space,
143 148 Some(&mut pointer_element),
144 149 dnd,
150 + preview,
145 151 pointer_location,
146 152 renderer,
147 153 &mut framebuffer,