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,