aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/config.rs5
-rw-r--r--src/input.rs106
-rw-r--r--src/main.rs1
-rw-r--r--src/niri.rs117
-rw-r--r--src/screenshot_ui.rs448
5 files changed, 662 insertions, 15 deletions
diff --git a/src/config.rs b/src/config.rs
index 0ffaeab1..dc1ceeaa 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -236,6 +236,11 @@ pub enum Action {
PowerOffMonitors,
ToggleDebugTint,
Spawn(#[knuffel(arguments)] Vec<String>),
+ #[knuffel(skip)]
+ ConfirmScreenshot,
+ #[knuffel(skip)]
+ CancelScreenshot,
+ Screenshot,
ScreenshotScreen,
ScreenshotWindow,
CloseWindow,
diff --git a/src/input.rs b/src/input.rs
index 436a7b01..f157d63b 100644
--- a/src/input.rs
+++ b/src/input.rs
@@ -19,6 +19,7 @@ use smithay::wayland::tablet_manager::{TabletDescriptor, TabletSeatTrait};
use crate::config::{Action, Binds, Modifiers};
use crate::niri::State;
+use crate::screenshot_ui::ScreenshotUi;
use crate::utils::{center, get_monotonic_time, spawn};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -70,6 +71,7 @@ impl State {
raw,
pressed,
*mods,
+ &this.niri.screenshot_ui,
)
},
) else {
@@ -129,6 +131,32 @@ impl State {
}
}
}
+ Action::ConfirmScreenshot => {
+ if let Some(renderer) = self.backend.renderer() {
+ match self.niri.screenshot_ui.capture(renderer) {
+ Ok((size, pixels)) => {
+ if let Err(err) = self.niri.save_screenshot(size, pixels) {
+ warn!("error saving screenshot: {err:?}");
+ }
+ }
+ Err(err) => {
+ warn!("error capturing screenshot: {err:?}");
+ }
+ }
+ }
+
+ self.niri.screenshot_ui.close();
+ self.niri.queue_redraw_all();
+ }
+ Action::CancelScreenshot => {
+ self.niri.screenshot_ui.close();
+ self.niri.queue_redraw_all();
+ }
+ Action::Screenshot => {
+ if let Some(renderer) = self.backend.renderer() {
+ self.niri.open_screenshot_ui(renderer);
+ }
+ }
Action::ScreenshotWindow => {
let active = self.niri.layout.active_window();
if let Some((window, output)) = active {
@@ -335,6 +363,21 @@ impl State {
}
}
+ if let Some(output) = self.niri.screenshot_ui.selection_output() {
+ let geom = self.niri.global_space.output_geometry(output).unwrap();
+ let mut point = new_pos;
+ point.x = point
+ .x
+ .clamp(geom.loc.x as f64, (geom.loc.x + geom.size.w - 1) as f64);
+ point.y = point
+ .y
+ .clamp(geom.loc.y as f64, (geom.loc.y + geom.size.h - 1) as f64);
+ let point = (point - geom.loc.to_f64())
+ .to_physical(output.current_scale().fractional_scale())
+ .to_i32_round();
+ self.niri.screenshot_ui.pointer_motion(point);
+ }
+
let under = self.niri.surface_under_and_global_space(new_pos);
self.niri.pointer_focus = under.clone();
let under = under.map(|u| u.surface);
@@ -378,6 +421,21 @@ impl State {
let pointer = self.niri.seat.get_pointer().unwrap();
+ if let Some(output) = self.niri.screenshot_ui.selection_output() {
+ let geom = self.niri.global_space.output_geometry(output).unwrap();
+ let mut point = pos;
+ point.x = point
+ .x
+ .clamp(geom.loc.x as f64, (geom.loc.x + geom.size.w - 1) as f64);
+ point.y = point
+ .y
+ .clamp(geom.loc.y as f64, (geom.loc.y + geom.size.h - 1) as f64);
+ let point = (point - geom.loc.to_f64())
+ .to_physical(output.current_scale().fractional_scale())
+ .to_i32_round();
+ self.niri.screenshot_ui.pointer_motion(point);
+ }
+
let under = self.niri.surface_under_and_global_space(pos);
self.niri.pointer_focus = under.clone();
let under = under.map(|u| u.surface);
@@ -418,6 +476,34 @@ impl State {
self.update_pointer_focus();
+ if let Some(button) = event.button() {
+ let pos = pointer.current_location();
+ if let Some((output, _)) = self.niri.output_under(pos) {
+ let output = output.clone();
+ let geom = self.niri.global_space.output_geometry(&output).unwrap();
+ let mut point = pos;
+ // Re-clamp as pointer can be within 0.5 from the limit which will round up
+ // to a wrong value.
+ point.x = point
+ .x
+ .clamp(geom.loc.x as f64, (geom.loc.x + geom.size.w - 1) as f64);
+ point.y = point
+ .y
+ .clamp(geom.loc.y as f64, (geom.loc.y + geom.size.h - 1) as f64);
+ let point = (point - geom.loc.to_f64())
+ .to_physical(output.current_scale().fractional_scale())
+ .to_i32_round();
+ if self.niri.screenshot_ui.pointer_button(
+ output,
+ point,
+ button,
+ button_state,
+ ) {
+ self.niri.queue_redraw_all();
+ }
+ }
+ }
+
pointer.button(
self,
&ButtonEvent {
@@ -842,6 +928,7 @@ fn should_intercept_key(
raw: Option<Keysym>,
pressed: bool,
mods: ModifiersState,
+ screenshot_ui: &ScreenshotUi,
) -> FilterResult<Option<Action>> {
// Actions are only triggered on presses, release of the key
// shouldn't try to intercept anything unless we have marked
@@ -850,7 +937,20 @@ fn should_intercept_key(
return FilterResult::Forward;
}
- match (action(bindings, comp_mod, modified, raw, mods), pressed) {
+ let mut final_action = action(bindings, comp_mod, modified, raw, mods);
+ if screenshot_ui.is_open()
+ // Allow only a subset of compositor actions while the screenshot UI is open,
+ // since the user cannot see the screen.
+ && !matches!(
+ final_action,
+ Some(Action::Quit | Action::ChangeVt(_) | Action::Suspend | Action::PowerOffMonitors)
+ )
+ {
+ // Otherwise, use the screenshot UI action.
+ final_action = screenshot_ui.action(raw, mods);
+ }
+
+ match (final_action, pressed) {
(Some(action), true) => {
suppressed_keys.insert(key_code);
FilterResult::Intercept(Some(action))
@@ -965,6 +1065,8 @@ mod tests {
let comp_mod = CompositorMod::Super;
let mut suppressed_keys = HashSet::new();
+ let screenshot_ui = ScreenshotUi::new();
+
// The key_code we pick is arbitrary, the only thing
// that matters is that they are different between cases.
@@ -979,6 +1081,7 @@ mod tests {
Some(close_keysym),
pressed,
mods,
+ &screenshot_ui,
)
};
@@ -993,6 +1096,7 @@ mod tests {
Some(Keysym::l),
pressed,
mods,
+ &screenshot_ui,
)
};
diff --git a/src/main.rs b/src/main.rs
index 57308f6b..3792caaf 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -12,6 +12,7 @@ mod handlers;
mod input;
mod layout;
mod niri;
+mod screenshot_ui;
mod utils;
mod watcher;
diff --git a/src/niri.rs b/src/niri.rs
index d2466c29..cdea4e49 100644
--- a/src/niri.rs
+++ b/src/niri.rs
@@ -20,6 +20,7 @@ use smithay::backend::renderer::element::{
RenderElementStates,
};
use smithay::backend::renderer::gles::{GlesMapping, GlesRenderer, GlesTexture};
+use smithay::backend::renderer::sync::SyncPoint;
use smithay::backend::renderer::{Bind, ExportMem, Frame, ImportAll, Offscreen, Renderer};
use smithay::desktop::utils::{
bbox_from_surface_tree, output_update, send_dmabuf_feedback_surface_tree,
@@ -85,6 +86,7 @@ use crate::frame_clock::FrameClock;
use crate::handlers::configure_lock_surface;
use crate::layout::{output_size, Layout, MonitorRenderElement};
use crate::pw_utils::{Cast, PipeWire};
+use crate::screenshot_ui::{ScreenshotUi, ScreenshotUiRenderElement};
use crate::utils::{center, get_monotonic_time, make_screenshot_path, write_png_rgba8};
const CLEAR_COLOR: [f32; 4] = [0.2, 0.2, 0.2, 1.];
@@ -150,6 +152,8 @@ pub struct Niri {
pub lock_state: LockState,
+ pub screenshot_ui: ScreenshotUi,
+
#[cfg(feature = "dbus")]
pub dbus: Option<crate::dbus::DBusServers>,
#[cfg(feature = "dbus")]
@@ -310,7 +314,7 @@ impl State {
let pointer = &self.niri.seat.get_pointer().unwrap();
let location = pointer.current_location();
- if !self.niri.is_locked() {
+ if !self.niri.is_locked() && !self.niri.screenshot_ui.is_open() {
// Don't refresh cursor focus during transitions.
if let Some((output, _)) = self.niri.output_under(location) {
let monitor = self.niri.layout.monitor_for_output(output).unwrap();
@@ -366,6 +370,8 @@ impl State {
pub fn update_focus(&mut self) {
let focus = if self.niri.is_locked() {
self.niri.lock_surface_focus()
+ } else if self.niri.screenshot_ui.is_open() {
+ None
} else {
self.niri.layer_surface_focus().or_else(|| {
self.niri
@@ -580,6 +586,8 @@ impl Niri {
let cursor_manager =
CursorManager::new(&config_.cursor.xcursor_theme, config_.cursor.xcursor_size);
+ let screenshot_ui = ScreenshotUi::new();
+
let socket_source = ListeningSocketSource::new_auto().unwrap();
let socket_name = socket_source.socket_name().to_os_string();
event_loop
@@ -657,6 +665,8 @@ impl Niri {
lock_state: LockState::Unlocked,
+ screenshot_ui,
+
#[cfg(feature = "dbus")]
dbus: None,
#[cfg(feature = "dbus")]
@@ -832,6 +842,10 @@ impl Niri {
}
lock_state => self.lock_state = lock_state,
}
+
+ if self.screenshot_ui.close() {
+ self.queue_redraw_all();
+ }
}
pub fn output_resized(&mut self, output: Output) {
@@ -852,6 +866,18 @@ impl Niri {
}
}
+ // If the output size changed with an open screenshot UI, close the screenshot UI.
+ if let Some(old_size) = self.screenshot_ui.output_size(&output) {
+ let output_transform = output.current_transform();
+ let output_mode = output.current_mode().unwrap();
+ let size = output_transform.transform_size(output_mode.size);
+ if old_size != size {
+ self.screenshot_ui.close();
+ self.queue_redraw_all();
+ return;
+ }
+ }
+
self.queue_redraw(output);
}
@@ -889,7 +915,7 @@ impl Niri {
}
pub fn window_under_cursor(&self) -> Option<&Window> {
- if self.is_locked() {
+ if self.is_locked() || self.screenshot_ui.is_open() {
return None;
}
@@ -928,6 +954,10 @@ impl Niri {
});
}
+ if self.screenshot_ui.is_open() {
+ return None;
+ }
+
let (window, win_pos_within_output) =
self.layout.window_under(&output, pos_within_output)?;
@@ -1321,6 +1351,32 @@ impl Niri {
return elements;
}
+ // Prepare the background element.
+ let state = self.output_state.get(output).unwrap();
+ let background = SolidColorRenderElement::from_buffer(
+ &state.background_buffer,
+ (0, 0),
+ output_scale,
+ 1.,
+ Kind::Unspecified,
+ )
+ .into();
+
+ // If the screenshot UI is open, draw it.
+ if self.screenshot_ui.is_open() {
+ elements.extend(
+ self.screenshot_ui
+ .render_output(output)
+ .into_iter()
+ .map(OutputRenderElements::from),
+ );
+
+ // Add the background for outputs that were connected while the screenshot UI was open.
+ elements.push(background);
+
+ return elements;
+ }
+
// Get monitor elements.
let mon = self.layout.monitor_for_output(output).unwrap();
let monitor_elements = mon.render_elements(renderer);
@@ -1363,17 +1419,7 @@ impl Niri {
extend_from_layer(&mut elements, Layer::Background);
// Then the background.
- let state = self.output_state.get(output).unwrap();
- elements.push(
- SolidColorRenderElement::from_buffer(
- &state.background_buffer,
- (0, 0),
- output_scale,
- 1.,
- Kind::Unspecified,
- )
- .into(),
- );
+ elements.push(background);
elements
}
@@ -1879,6 +1925,42 @@ impl Niri {
}
}
+ pub fn open_screenshot_ui(&mut self, renderer: &mut GlesRenderer) {
+ if self.is_locked() || self.screenshot_ui.is_open() {
+ return;
+ }
+
+ let Some(default_output) = self.output_under_cursor() else {
+ return;
+ };
+
+ let screenshots = self
+ .global_space
+ .outputs()
+ .cloned()
+ .filter_map(|output| {
+ let size = output.current_mode().unwrap().size;
+ let scale = Scale::from(output.current_scale().fractional_scale());
+ let elements = self.render(renderer, &output, true);
+
+ let res = render_to_texture(renderer, size, scale, Fourcc::Abgr8888, &elements);
+ let screenshot = match res {
+ Ok((texture, _)) => texture,
+ Err(err) => {
+ warn!("error rendering output {}: {err:?}", output.name());
+ return None;
+ }
+ };
+
+ Some((output, screenshot))
+ })
+ .collect();
+
+ self.screenshot_ui
+ .open(renderer, screenshots, default_output);
+ self.queue_redraw_all();
+ }
+
pub fn screenshot(&self, renderer: &mut GlesRenderer, output: &Output) -> anyhow::Result<()> {
let _span = tracy_client::span!("Niri::screenshot");
@@ -1916,7 +1998,11 @@ impl Niri {
.context("error saving screenshot")
}
- fn save_screenshot(&self, size: Size<i32, Physical>, pixels: Vec<u8>) -> anyhow::Result<()> {
+ pub fn save_screenshot(
+ &self,
+ size: Size<i32, Physical>,
+ pixels: Vec<u8>,
+ ) -> anyhow::Result<()> {
let path = make_screenshot_path().context("error making screenshot path")?;
debug!("saving screenshot to {path:?}");
@@ -2029,6 +2115,8 @@ impl Niri {
pub fn lock(&mut self, confirmation: SessionLocker) {
info!("locking session");
+ self.screenshot_ui.close();
+
self.lock_state = LockState::Locking(confirmation);
self.queue_redraw_all();
}
@@ -2065,6 +2153,7 @@ render_elements! {
Wayland = WaylandSurfaceRenderElement<R>,
NamedPointer = TextureRenderElement<<R as Renderer>::TextureId>,
SolidColor = SolidColorRenderElement,
+ ScreenshotUi = ScreenshotUiRenderElement<R>,
}
#[derive(Default)]
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))
+}