From bd58d2c1b7d0ed68ea4eedefcea82675b18d2567 Mon Sep 17 00:00:00 2001 From: elwrcl Date: Sat, 3 Oct 2026 02:31:28 +0300 Subject: [PATCH] feat: size label while selecting a region --- src/region.rs | 31 +++++++++++++- src/render.rs | 111 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 140 insertions(+), 2 deletions(-) diff --git a/src/region.rs b/src/region.rs index bf2d88a..14b0efb 100644 --- a/src/region.rs +++ b/src/region.rs @@ -50,6 +50,8 @@ pub struct Overlay { anchor: Option, /// Shift, Alt and Space as the keyboard last reported them. mods: Mods, + /// The surface the pointer is on and its surface-local position. + pointer: Option<(usize, (f64, f64))>, outcome: Option>, } @@ -112,6 +114,7 @@ pub fn select(wl: &mut Wayland, outputs: &[Output], frames: &[Frame]) -> Result< selection: Selection::default(), anchor: None, mods: Mods::default(), + pointer: None, outcome: None, }); let result = wl.dispatch_blocking(|s| s.overlay.as_ref().is_some_and(|o| o.outcome.is_some())); @@ -143,7 +146,12 @@ impl Surface { }) } - fn draw(&mut self, sel: Option, qh: &QueueHandle) { + fn draw( + &mut self, + sel: Option, + label: Option<(PixelRect, (f64, f64))>, + qh: &QueueHandle, + ) { let stride = self.width as i32 * 4; let Ok((buffer, canvas)) = self.pool.create_buffer( self.width as i32, @@ -155,6 +163,22 @@ impl Surface { return; }; render::compose(canvas, &self.dark, self.bright.as_raw(), self.width, sel); + if let Some((rect, (px, py))) = label { + let scale = self.width as f64 / self.geom.width as f64; + let text = render::size_text(rect); + let size = ( + ((crate::toolbar::draw::measure(&text) + 12.0) as f64 * scale).ceil() as u32, + ((crate::toolbar::draw::LABEL * 1.2 + 12.0) as f64 * scale).ceil() as u32, + ); + let pill = crate::toolbar::draw::pill(size, scale as f32, &text); + let at = render::label_origin( + (px * scale, py * scale), + (pill.width(), pill.height()), + (self.width, self.height), + 16.0 * scale, + ); + render::blend(canvas, self.width, &pill, at); + } let surface = self.layer.wl_surface(); surface.damage_buffer(0, 0, self.width as i32, self.height as i32); @@ -223,10 +247,12 @@ impl Overlay { fn redraw(&mut self, i: usize, qh: &QueueHandle) { let sel = self.selection_on(i); + // The size label goes next to the pointer, on the output it is on. + let label = sel.zip(self.pointer.filter(|(on, _)| *on == i).map(|(_, at)| at)); let s = &mut self.surfaces[i]; s.dirty = true; if s.configured && !s.frame_pending { - s.draw(sel, qh); + s.draw(sel, label, qh); } } @@ -268,6 +294,7 @@ impl Overlay { let Some(i) = self.index_of(&event.surface) else { continue; }; + self.pointer = Some((i, event.position)); let g = &self.surfaces[i].geom; let p = Point { x: g.x as f64 + event.position.0, diff --git a/src/render.rs b/src/render.rs index 111bb50..20dd70b 100644 --- a/src/render.rs +++ b/src/render.rs @@ -36,6 +36,58 @@ pub fn compose(dst: &mut [u8], dark: &[u8], bright: &[u8], width: u32, sel: Opti } } +/// Where a `label`-sized box goes next to the pointer: `gap` px right of and +/// below it, or on the other side where it would leave the surface. +pub fn label_origin( + pointer: (f64, f64), + label: (u32, u32), + surface: (u32, u32), + gap: f64, +) -> (u32, u32) { + let place = |p: f64, len: u32, max: u32| { + let after = p + gap; + let v = if after + len as f64 <= max as f64 { + after + } else { + p - gap - len as f64 + }; + v.clamp(0.0, max.saturating_sub(len) as f64) as u32 + }; + ( + place(pointer.0, label.0, surface.0), + place(pointer.1, label.1, surface.1), + ) +} + +/// `W × H` of a selection in physical pixels. +pub fn size_text(r: crate::frame::PixelRect) -> String { + format!("{} × {}", r.width, r.height) +} + +/// Draws premultiplied `src` over the XRGB8888 `dst` at `at`, clipped. +pub fn blend(dst: &mut [u8], dst_width: u32, src: &tiny_skia::Pixmap, at: (u32, u32)) { + let dst_height = (dst.len() / 4) as u32 / dst_width.max(1); + for sy in 0..src.height() { + let y = at.1 + sy; + if y >= dst_height { + break; + } + for sx in 0..src.width() { + let x = at.0 + sx; + if x >= dst_width { + break; + } + let s = src.pixel(sx, sy).expect("in bounds"); + let i = ((y * dst_width + x) * 4) as usize; + let inv = 255 - s.alpha() as u16; + let over = |d: u8, c: u8| (c as u16 + (d as u16 * inv + 127) / 255) as u8; + dst[i] = over(dst[i], s.blue()); + dst[i + 1] = over(dst[i + 1], s.green()); + dst[i + 2] = over(dst[i + 2], s.red()); + } + } +} + #[cfg(test)] mod tests { use super::*; @@ -96,4 +148,63 @@ mod tests { compose(&mut dst, &dark, &[2u8; 16], 2, Some(sel)); assert_eq!(px(&dst, 2, 1, 1), [255; 4]); } + + #[test] + fn label_sits_below_right_of_the_pointer() { + assert_eq!( + label_origin((100.0, 100.0), (80, 24), (1920, 1080), 16.0), + (116, 116) + ); + } + + #[test] + fn label_flips_at_edges() { + assert_eq!( + label_origin((1900.0, 100.0), (80, 24), (1920, 1080), 16.0), + (1900 - 16 - 80, 116) + ); + assert_eq!( + label_origin((100.0, 1070.0), (80, 24), (1920, 1080), 16.0), + (116, 1070 - 16 - 24) + ); + assert_eq!( + label_origin((1900.0, 1070.0), (80, 24), (1920, 1080), 16.0), + (1804, 1030) + ); + // Tiny outputs: never negative. + assert_eq!(label_origin((5.0, 5.0), (80, 24), (60, 20), 16.0), (0, 0)); + } + + #[test] + fn size_text_uses_the_multiplication_sign() { + let r = PixelRect { + x: 3, + y: 4, + width: 640, + height: 480, + }; + assert_eq!(size_text(r), "640 × 480"); + } + + #[test] + fn blend_draws_over_and_clips() { + let (w, h) = (4u32, 2u32); + let mut dst = vec![100u8; (w * h * 4) as usize]; + let mut src = tiny_skia::Pixmap::new(2, 2).unwrap(); + src.fill(tiny_skia::Color::from_rgba8(255, 0, 0, 255)); + blend(&mut dst, w, &src, (3, 0)); + assert_eq!(px(&dst, w, 3, 0)[..3], [0, 0, 255], "BGR red, opaque"); + assert_eq!( + px(&dst, w, 2, 0)[..3], + [100, 100, 100], + "left of it untouched" + ); + let mut half = tiny_skia::Pixmap::new(1, 1).unwrap(); + half.fill(tiny_skia::Color::from_rgba8(0, 0, 0, 128)); + blend(&mut dst, w, &half, (0, 1)); + assert!( + (px(&dst, w, 0, 1)[0] as i32 - 50).abs() <= 1, + "half black over 100" + ); + } }