diff options
Diffstat (limited to 'src/screenshot_ui.rs')
| -rw-r--r-- | src/screenshot_ui.rs | 448 |
1 files changed, 448 insertions, 0 deletions
diff --git a/src/screenshot_ui.rs b/src/screenshot_ui.rs new file mode 100644 index 00000000..3b0807fe --- /dev/null +++ b/src/screenshot_ui.rs @@ -0,0 +1,448 @@ +use std::cmp::{max, min}; +use std::collections::HashMap; +use std::iter::zip; +use std::mem; + +use anyhow::Context; +use arrayvec::ArrayVec; +use smithay::backend::allocator::Fourcc; +use smithay::backend::input::{ButtonState, MouseButton}; +use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement}; +use smithay::backend::renderer::element::texture::{TextureBuffer, TextureRenderElement}; +use smithay::backend::renderer::element::Kind; +use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture}; +use smithay::backend::renderer::ExportMem; +use smithay::input::keyboard::{Keysym, ModifiersState}; +use smithay::output::{Output, WeakOutput}; +use smithay::render_elements; +use smithay::utils::{Physical, Point, Rectangle, Size, Transform}; + +use crate::config::Action; + +const BORDER: i32 = 2; + +// Ideally the screenshot UI should support cross-output selections. However, that poses some +// technical challenges when the outputs have different scales and such. So, this implementation +// allows only single-output selections for now. +// +// As a consequence of this, selection coordinates are in output-local coordinate space. +pub enum ScreenshotUi { + Closed { + last_selection: Option<(WeakOutput, Rectangle<i32, Physical>)>, + }, + Open { + selection: (Output, Point<i32, Physical>, Point<i32, Physical>), + output_data: HashMap<Output, OutputData>, + mouse_down: bool, + }, +} + +pub struct OutputData { + size: Size<i32, Physical>, + texture: GlesTexture, + texture_buffer: TextureBuffer<GlesTexture>, + buffers: [SolidColorBuffer; 8], + locations: [Point<i32, Physical>; 8], +} + +render_elements! { + #[derive(Debug)] + pub ScreenshotUiRenderElement<R>; + Screenshot = TextureRenderElement<R::TextureId>, + SolidColor = SolidColorRenderElement, +} + +impl ScreenshotUi { + pub fn new() -> Self { + Self::Closed { + last_selection: None, + } + } + + pub fn open( + &mut self, + renderer: &GlesRenderer, + screenshots: HashMap<Output, GlesTexture>, + default_output: Output, + ) -> bool { + if screenshots.is_empty() { + return false; + } + + let Self::Closed { last_selection } = self else { + return false; + }; + + let last_selection = last_selection + .take() + .and_then(|(weak, sel)| weak.upgrade().map(|output| (output, sel))); + let selection = match last_selection { + Some(selection) if screenshots.contains_key(&selection.0) => selection, + _ => { + let output = default_output; + let output_transform = output.current_transform(); + let output_mode = output.current_mode().unwrap(); + let size = output_transform.transform_size(output_mode.size); + ( + output, + Rectangle::from_loc_and_size( + (size.w / 4, size.h / 4), + (size.w / 2, size.h / 2), + ), + ) + } + }; + let scale = selection.0.current_scale().integer_scale(); + let selection = ( + selection.0, + selection.1.loc, + selection.1.loc + selection.1.size - Size::from((scale, scale)), + ); + + let output_data = screenshots + .into_iter() + .map(|(output, texture)| { + let output_transform = output.current_transform(); + let output_mode = output.current_mode().unwrap(); + let size = output_transform.transform_size(output_mode.size); + let texture_buffer = TextureBuffer::from_texture( + renderer, + texture.clone(), + output.current_scale().integer_scale(), + Transform::Normal, + None, + ); + let buffers = [ + SolidColorBuffer::new((0, 0), [1., 1., 1., 1.]), + SolidColorBuffer::new((0, 0), [1., 1., 1., 1.]), + SolidColorBuffer::new((0, 0), [1., 1., 1., 1.]), + SolidColorBuffer::new((0, 0), [1., 1., 1., 1.]), + SolidColorBuffer::new((0, 0), [0., 0., 0., 0.5]), + SolidColorBuffer::new((0, 0), [0., 0., 0., 0.5]), + SolidColorBuffer::new((0, 0), [0., 0., 0., 0.5]), + SolidColorBuffer::new((0, 0), [0., 0., 0., 0.5]), + ]; + let locations = [Default::default(); 8]; + let data = OutputData { + size, + texture, + texture_buffer, + buffers, + locations, + }; + (output, data) + }) + .collect(); + + *self = Self::Open { + selection, + output_data, + mouse_down: false, + }; + + self.update_buffers(); + + true + } + + pub fn close(&mut self) -> bool { + let selection = match mem::take(self) { + Self::Open { selection, .. } => selection, + closed @ Self::Closed { .. } => { + // Put it back. + *self = closed; + return false; + } + }; + + let scale = selection.0.current_scale().integer_scale(); + let last_selection = Some(( + selection.0.downgrade(), + rect_from_corner_points(selection.1, selection.2, scale), + )); + + *self = Self::Closed { last_selection }; + + true + } + + pub fn is_open(&self) -> bool { + matches!(self, ScreenshotUi::Open { .. }) + } + + fn update_buffers(&mut self) { + let Self::Open { + selection, + output_data, + .. + } = self + else { + panic!("screenshot UI must be open to update buffers"); + }; + + let (selection_output, a, b) = selection; + let scale = selection_output.current_scale().integer_scale(); + let mut rect = rect_from_corner_points(*a, *b, scale); + + for (output, data) in output_data { + let buffers = &mut data.buffers; + let locations = &mut data.locations; + let size = data.size; + + if output == selection_output { + let scale = output.current_scale().integer_scale(); + + // Check if the selection is still valid. If not, reset it back to default. + if !Rectangle::from_loc_and_size((0, 0), size).contains_rect(rect) { + rect = Rectangle::from_loc_and_size( + (size.w / 4, size.h / 4), + (size.w / 2, size.h / 2), + ); + *a = rect.loc; + *b = rect.loc + rect.size - Size::from((scale, scale)); + } + + let border = BORDER * scale; + + buffers[0].resize((rect.size.w + border * 2, border)); + buffers[1].resize((rect.size.w + border * 2, border)); + buffers[2].resize((border, rect.size.h)); + buffers[3].resize((border, rect.size.h)); + + buffers[4].resize((size.w, rect.loc.y)); + buffers[5].resize((size.w, size.h - rect.loc.y - rect.size.h)); + buffers[6].resize((rect.loc.x, rect.size.h)); + buffers[7].resize((size.w - rect.loc.x - rect.size.w, rect.size.h)); + + locations[0] = Point::from((rect.loc.x - border, rect.loc.y - border)); + locations[1] = Point::from((rect.loc.x - border, rect.loc.y + rect.size.h)); + locations[2] = Point::from((rect.loc.x - border, rect.loc.y)); + locations[3] = Point::from((rect.loc.x + rect.size.w, rect.loc.y)); + + locations[5] = Point::from((0, rect.loc.y + rect.size.h)); + locations[6] = Point::from((0, rect.loc.y)); + locations[7] = Point::from((rect.loc.x + rect.size.w, rect.loc.y)); + } else { + buffers[0].resize((0, 0)); + buffers[1].resize((0, 0)); + buffers[2].resize((0, 0)); + buffers[3].resize((0, 0)); + + buffers[4].resize(size.to_logical(1)); + buffers[5].resize((0, 0)); + buffers[6].resize((0, 0)); + buffers[7].resize((0, 0)); + } + } + } + + pub fn render_output( + &self, + output: &Output, + ) -> ArrayVec<ScreenshotUiRenderElement<GlesRenderer>, 9> { + let _span = tracy_client::span!("ScreenshotUi::render_output"); + + let Self::Open { output_data, .. } = self else { + panic!("screenshot UI must be open to render it"); + }; + + let mut elements = ArrayVec::new(); + + let Some(output_data) = output_data.get(output) else { + return elements; + }; + + let buf_loc = zip(&output_data.buffers, &output_data.locations); + elements.extend(buf_loc.map(|(buffer, loc)| { + SolidColorRenderElement::from_buffer( + buffer, + *loc, + 1., // We treat these as physical coordinates. + 1., + Kind::Unspecified, + ) + .into() + })); + + // The screenshot itself goes last. + elements.push( + TextureRenderElement::from_texture_buffer( + (0., 0.), + &output_data.texture_buffer, + None, + None, + None, + Kind::Unspecified, + ) + .into(), + ); + + elements + } + + pub fn capture( + &self, + renderer: &mut GlesRenderer, + ) -> anyhow::Result<(Size<i32, Physical>, Vec<u8>)> { + let _span = tracy_client::span!("ScreenshotUi::capture"); + + let Self::Open { + selection, + output_data, + .. + } = self + else { + panic!("screenshot UI must be open to capture"); + }; + + let data = &output_data[&selection.0]; + let scale = selection.0.current_scale().integer_scale(); + let rect = rect_from_corner_points(selection.1, selection.2, scale); + let buf_rect = rect + .to_logical(1) + .to_buffer(1, Transform::Normal, &data.size.to_logical(1)); + + let mapping = renderer + .copy_texture(&data.texture, buf_rect, Fourcc::Abgr8888) + .context("error copying texture")?; + let copy = renderer + .map_texture(&mapping) + .context("error mapping texture")?; + + Ok((rect.size, copy.to_vec())) + } + + pub fn action(&self, raw: Option<Keysym>, mods: ModifiersState) -> Option<Action> { + if !matches!(self, Self::Open { .. }) { + return None; + } + + action(raw?, mods) + } + + pub fn selection_output(&self) -> Option<&Output> { + if let Self::Open { + selection: (output, _, _), + .. + } = self + { + Some(output) + } else { + None + } + } + + pub fn output_size(&self, output: &Output) -> Option<Size<i32, Physical>> { + if let Self::Open { output_data, .. } = self { + Some(output_data.get(output)?.size) + } else { + None + } + } + + /// The pointer has moved to `point` relative to the current selection output. + pub fn pointer_motion(&mut self, point: Point<i32, Physical>) { + let Self::Open { + selection, + mouse_down: true, + .. + } = self + else { + return; + }; + + selection.2 = point; + self.update_buffers(); + } + + pub fn pointer_button( + &mut self, + output: Output, + point: Point<i32, Physical>, + button: MouseButton, + state: ButtonState, + ) -> bool { + let Self::Open { + selection, + output_data, + mouse_down, + } = self + else { + return false; + }; + + if button != MouseButton::Left { + return false; + } + + let down = state == ButtonState::Pressed; + if *mouse_down == down { + return false; + } + + if !output_data.contains_key(&output) { + return false; + } + + *mouse_down = down; + + if down { + *selection = (output, point, point); + } else { + // Check if the resulting selection is zero-sized, and try to come up with a small + // default rectangle. + let (output, a, b) = selection; + let scale = output.current_scale().integer_scale(); + let mut rect = rect_from_corner_points(*a, *b, scale); + if rect.size.is_empty() || rect.size == Size::from((scale, scale)) { + let data = &output_data[output]; + rect = Rectangle::from_loc_and_size((rect.loc.x - 16, rect.loc.y - 16), (32, 32)) + .intersection(Rectangle::from_loc_and_size((0, 0), data.size)) + .unwrap_or_default(); + let scale = output.current_scale().integer_scale(); + *a = rect.loc; + *b = rect.loc + rect.size - Size::from((scale, scale)); + } + } + + self.update_buffers(); + + true + } +} + +impl Default for ScreenshotUi { + fn default() -> Self { + Self::new() + } +} + +fn action(raw: Keysym, mods: ModifiersState) -> Option<Action> { + if raw == Keysym::Escape { + return Some(Action::CancelScreenshot); + } + + if mods.alt || mods.shift { + return None; + } + + if (mods.ctrl && raw == Keysym::c) + || (!mods.ctrl && (raw == Keysym::space || raw == Keysym::Return)) + { + return Some(Action::ConfirmScreenshot); + } + + None +} + +pub fn rect_from_corner_points( + a: Point<i32, Physical>, + b: Point<i32, Physical>, + scale: i32, +) -> Rectangle<i32, Physical> { + let x1 = min(a.x, b.x); + let y1 = min(a.y, b.y); + let x2 = max(a.x, b.x); + let y2 = max(a.y, b.y); + Rectangle::from_extemities((x1, y1), (x2 + scale, y2 + scale)) +} |
