use std::cell::RefCell; use std::cmp::{max, min}; use std::collections::HashMap; use std::f64::consts::TAU; use std::iter::zip; use std::rc::Rc; use anyhow::Context; use arrayvec::ArrayVec; use niri_config::{Action, Config}; use niri_ipc::SizeChange; use pango::{Alignment, FontDescription}; use pangocairo::cairo::{self, ImageSurface}; use smithay::backend::allocator::Fourcc; use smithay::backend::input::TouchSlot; use smithay::backend::renderer::element::utils::{Relocate, RelocateRenderElement}; use smithay::backend::renderer::element::Kind; use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture}; use smithay::backend::renderer::{ExportMem, Texture as _}; use smithay::input::keyboard::{Keysym, ModifiersState}; use smithay::output::{Output, WeakOutput}; use smithay::utils::{Buffer, Physical, Point, Rectangle, Scale, Size, Transform}; use crate::animation::{Animation, Clock}; use crate::layout::floating::DIRECTIONAL_MOVE_PX; use crate::niri_render_elements; use crate::render_helpers::primary_gpu_texture::PrimaryGpuTextureRenderElement; use crate::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement}; use crate::render_helpers::texture::{TextureBuffer, TextureRenderElement}; use crate::render_helpers::{render_to_texture, RenderTarget}; use crate::utils::to_physical_precise_round; const SELECTION_BORDER: i32 = 2; const PADDING: i32 = 8; const RADIUS: i32 = 16; const FONT: &str = "sans 14px"; const BORDER: i32 = 4; const TEXT_HIDE_P: &str = "Press Space to save the screenshot.\n\ Press P to hide the pointer."; const TEXT_SHOW_P: &str = "Press Space to save the screenshot.\n\ Press P to show the pointer."; // 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. #[allow(clippy::large_enum_variant)] pub enum ScreenshotUi { Closed { last_selection: Option<(WeakOutput, Rectangle)>, clock: Clock, config: Rc>, }, Open { selection: (Output, Point, Point), output_data: HashMap, button: Button, show_pointer: bool, open_anim: Animation, clock: Clock, config: Rc>, path: Option, }, } /// State for moving the selection (as opposed to just drawing). pub struct MoveState { // Cursor offset from selection.1 when starting the move. pointer_offset: Point, // If the move is initiated by a touch, this is the slot. If `None`, the move was initiated by // holding Space. touch_slot: Option, } pub enum Button { Up, Down { touch_slot: Option, on_capture_button: bool, last_pos: (Output, Point), move_state: Option, }, } pub struct OutputData { size: Size, scale: f64, transform: Transform, // Output, screencast, screen capture. screenshot: [OutputScreenshot; 3], buffers: [SolidColorBuffer; 8], locations: [Point; 8], panel: Option<(TextureBuffer, TextureBuffer)>, } pub struct OutputScreenshot { texture: GlesTexture, buffer: PrimaryGpuTextureRenderElement, pointer: Option, } niri_render_elements! { ScreenshotUiRenderElement => { Screenshot = PrimaryGpuTextureRenderElement, SolidColor = SolidColorRenderElement, } } impl Button { fn is_down(&self) -> bool { matches!(self, Self::Down { .. }) } fn is_dragging_selection(&self) -> bool { matches!( self, Self::Down { on_capture_button: false, .. } ) } } impl ScreenshotUi { pub fn new(clock: Clock, config: Rc>) -> Self { Self::Closed { last_selection: None, clock, config, } } pub fn open( &mut self, renderer: &mut GlesRenderer, // Output, screencast, screen capture. screenshots: HashMap, default_output: Output, show_pointer: bool, path: Option, ) -> bool { if screenshots.is_empty() { return false; } let Self::Closed { last_selection, clock, config, } = 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::new( Point::from((size.w / 4, size.h / 4)), Size::from((size.w / 2, size.h / 2)), ), ) } }; let selection = ( selection.0, selection.1.loc, selection.1.loc + selection.1.size - Size::from((1, 1)), ); let output_data = screenshots .into_iter() .map(|(output, screenshot)| { let transform = output.current_transform(); let output_mode = output.current_mode().unwrap(); let size = transform.transform_size(output_mode.size); let scale = output.current_scale().fractional_scale(); 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 mut render_panel_ = |text| { render_panel(renderer, scale, text) .map_err(|err| warn!("error rendering help panel: {err:?}")) .ok() }; let panel_show = render_panel_(TEXT_SHOW_P); let panel_hide = render_panel_(TEXT_HIDE_P); let panel = Option::zip(panel_show, panel_hide); let data = OutputData { size, scale, transform, screenshot, buffers, locations, panel, }; (output, data) }) .collect(); let open_anim = { let c = config.borrow(); Animation::new(clock.clone(), 0., 1., 0., c.animations.screenshot_ui_open.0) }; *self = Self::Open { selection, output_data, button: Button::Up, show_pointer, open_anim, clock: clock.clone(), config: config.clone(), path, }; self.update_buffers(); true } pub fn close(&mut self) -> bool { let Self::Open { selection, clock, config, .. } = self else { return false; }; let last_selection = Some(( selection.0.downgrade(), rect_from_corner_points(selection.1, selection.2), )); *self = Self::Closed { last_selection, clock: clock.clone(), config: config.clone(), }; true } pub fn toggle_pointer(&mut self) { if let Self::Open { show_pointer, .. } = self { *show_pointer = !*show_pointer; } } pub fn is_open(&self) -> bool { matches!(self, ScreenshotUi::Open { .. }) } pub fn set_space_down(&mut self, down: bool) { if let Self::Open { selection, button: Button::Down { move_state, last_pos, .. }, .. } = self { if down { if move_state.is_none() { *move_state = Some(MoveState { pointer_offset: last_pos.1 - selection.1, touch_slot: None, }); } } else { // Only clear if moving with Space. if let Some(MoveState { touch_slot: None, .. }) = move_state { *move_state = None; } } } } pub fn move_left(&mut self) { let Self::Open { selection: (output, a, b), output_data, .. } = self else { return; }; let data = &output_data[output]; let delta: i32 = to_physical_precise_round(data.scale, DIRECTIONAL_MOVE_PX); let delta = min(delta, min(a.x, b.x)); a.x -= delta; b.x -= delta; self.update_buffers(); } pub fn move_right(&mut self) { let Self::Open { selection: (output, a, b), output_data, .. } = self else { return; }; let data = &output_data[output]; let delta: i32 = to_physical_precise_round(data.scale, DIRECTIONAL_MOVE_PX); let delta = min(delta, data.size.w - max(a.x, b.x) - 1); a.x += delta; b.x += delta; self.update_buffers(); } pub fn move_up(&mut self) { let Self::Open { selection: (output, a, b), output_data, .. } = self else { return; }; let data = &output_data[output]; let delta: i32 = to_physical_precise_round(data.scale, DIRECTIONAL_MOVE_PX); let delta = min(delta, min(a.y, b.y)); a.y -= delta; b.y -= delta; self.update_buffers(); } pub fn move_down(&mut self) { let Self::Open { selection: (output, a, b), output_data, .. } = self else { return; }; let data = &output_data[output]; let delta: i32 = to_physical_precise_round(data.scale, DIRECTIONAL_MOVE_PX); let delta = min(delta, data.size.h - max(a.y, b.y) - 1); a.y += delta; b.y += delta; self.update_buffers(); } /// Moves the screenshot selection to a different output. /// /// This preserves the relative position while keeping logical size. It is (intentionally) very /// similar to how floating windows move across monitors, but with one difference: floating /// windows can go partially outside the view, while the screenshot selection cannot. So, we /// clamp it to new output bounds, trying to preserve the size if possible. pub fn move_to_output(&mut self, new_output: Output) { let Self::Open { selection, output_data, .. } = self else { return; }; let (current_output, current_a, current_b) = selection; if current_output == &new_output { return; } let Some(target_data) = output_data.get(&new_output) else { return; }; let current_data = &output_data[current_output]; let current_rect: Rectangle<_, Physical> = Rectangle::new( Point::from((current_a.x.min(current_b.x), current_a.y.min(current_b.y))), Size::from(( (current_a.x.max(current_b.x) - current_a.x.min(current_b.x) + 1), (current_a.y.max(current_b.y) - current_a.y.min(current_b.y) + 1), )), ); let current_rect = current_rect.to_f64(); let rel_x = current_rect.loc.x / current_data.size.w as f64; let rel_y = current_rect.loc.y / current_data.size.h as f64; let factor = target_data.scale / current_data.scale; let mut new_width = (current_rect.size.w * factor).round() as i32; let mut new_height = (current_rect.size.h * factor).round() as i32; new_width = new_width.clamp(1, target_data.size.w); new_height = new_height.clamp(1, target_data.size.h); let new_x = (rel_x * target_data.size.w as f64).round() as i32; let new_y = (rel_y * target_data.size.h as f64).round() as i32; let max_x = target_data.size.w - new_width; let max_y = target_data.size.h - new_height; let new_x = new_x.clamp(0, max_x); let new_y = new_y.clamp(0, max_y); let new_rect = Rectangle::new( Point::from((new_x, new_y)), Size::from((new_width, new_height)), ); *selection = ( new_output, new_rect.loc, new_rect.loc + new_rect.size - Size::from((1, 1)), ); self.update_buffers(); } pub fn select_entire_output(&mut self) { let Self::Open { selection, output_data, .. } = self else { return; }; let current_data = &output_data[&selection.0]; let size = current_data.size; selection.1 = Point::new(0, 0); selection.2 = Point::new(size.w - 1, size.h - 1); self.update_buffers(); } pub fn set_width(&mut self, change: SizeChange) { let Self::Open { selection: (output, a, b), output_data, .. } = self else { return; }; let data = &output_data[output]; let available_size = f64::from(data.size.w); let current_size = max(a.x, b.x) + 1 - min(a.x, b.x); let new_size = match change { SizeChange::SetFixed(fixed) => to_physical_precise_round(data.scale, fixed), SizeChange::SetProportion(prop) => { let prop = (prop / 100.).clamp(0., 1.); (available_size * prop).round() as i32 } SizeChange::AdjustFixed(delta) => { let delta = to_physical_precise_round(data.scale, delta); current_size.saturating_add(delta) } SizeChange::AdjustProportion(delta) => { let current_prop = f64::from(current_size) / available_size; let prop = (current_prop + delta / 100.).clamp(0., 1.); (available_size * prop).round() as i32 } }; let new_size = new_size.clamp(1, data.size.w - min(a.x, b.x)) - 1; a.x = min(a.x, b.x); b.x = a.x + new_size; self.update_buffers(); } pub fn set_height(&mut self, change: SizeChange) { let Self::Open { selection: (output, a, b), output_data, .. } = self else { return; }; let data = &output_data[output]; let available_size = f64::from(data.size.h); let current_size = max(a.y, b.y) + 1 - min(a.y, b.y); let new_size = match change { SizeChange::SetFixed(fixed) => to_physical_precise_round(data.scale, fixed), SizeChange::SetProportion(prop) => { let prop = (prop / 100.).clamp(0., 1.); (available_size * prop).round() as i32 } SizeChange::AdjustFixed(delta) => { let delta = to_physical_precise_round(data.scale, delta); current_size.saturating_add(delta) } SizeChange::AdjustProportion(delta) => { let current_prop = f64::from(current_size) / available_size; let prop = (current_prop + delta / 100.).clamp(0., 1.); (available_size * prop).round() as i32 } }; let new_size = new_size.clamp(1, data.size.h - min(a.y, b.y)) - 1; a.y = min(a.y, b.y); b.y = a.y + new_size; self.update_buffers(); } pub fn advance_animations(&mut self) {} pub fn are_animations_ongoing(&self) -> bool { let Self::Open { open_anim, .. } = self else { return false; }; !open_anim.is_done() } 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 mut rect = rect_from_corner_points(*a, *b); for (output, data) in output_data { let buffers = &mut data.buffers; let locations = &mut data.locations; let size = data.size; let scale = data.scale; if output == selection_output { // Check if the selection is still valid. If not, reset it back to default. if !Rectangle::from_size(size).contains_rect(rect) { rect = Rectangle::new( Point::from((size.w / 4, size.h / 4)), Size::from((size.w / 2, size.h / 2)), ); *a = rect.loc; *b = rect.loc + rect.size - Size::from((1, 1)); } let border = to_physical_precise_round(scale, SELECTION_BORDER); let resize = move |buffer: &mut SolidColorBuffer, w: i32, h: i32| { let size = Size::<_, Physical>::from((w, h)); buffer.resize(size.to_f64().to_logical(scale)); }; resize(&mut buffers[0], rect.size.w + border * 2, border); resize(&mut buffers[1], rect.size.w + border * 2, border); resize(&mut buffers[2], border, rect.size.h); resize(&mut buffers[3], border, rect.size.h); resize(&mut buffers[4], size.w, rect.loc.y); resize(&mut buffers[5], size.w, size.h - rect.loc.y - rect.size.h); resize(&mut buffers[6], rect.loc.x, rect.size.h); resize( &mut buffers[7], 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_f64().to_logical(data.scale)); buffers[5].resize((0., 0.)); buffers[6].resize((0., 0.)); buffers[7].resize((0., 0.)); } } } pub fn render_output( &self, output: &Output, target: RenderTarget, ) -> ArrayVec { let _span = tracy_client::span!("ScreenshotUi::render_output"); let Self::Open { output_data, show_pointer, button, open_anim, .. } = 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 scale = output_data.scale; let progress = open_anim.clamped_value().clamp(0., 1.) as f32; // The help panel goes on top. if let Some((show, hide)) = &output_data.panel { let buffer = if *show_pointer { hide } else { show }; let alpha = if button.is_dragging_selection() { 0.3 } else { 0.9 }; let location = panel_location(output_data, buffer.texture().size()) .to_f64() .to_logical(scale); let elem = PrimaryGpuTextureRenderElement(TextureRenderElement::from_texture_buffer( buffer.clone(), location, alpha * progress, None, None, Kind::Unspecified, )); elements.push(elem.into()); } let buf_loc = zip(&output_data.buffers, &output_data.locations); elements.extend(buf_loc.map(|(buffer, loc)| { SolidColorRenderElement::from_buffer( buffer, loc.to_f64().to_logical(scale), progress, Kind::Unspecified, ) .into() })); // The screenshot itself goes last. let index = match target { RenderTarget::Output => 0, RenderTarget::Screencast => 1, RenderTarget::ScreenCapture => 2, }; let screenshot = &output_data.screenshot[index]; if *show_pointer { if let Some(pointer) = screenshot.pointer.clone() { elements.push(pointer.into()); } } elements.push(screenshot.buffer.clone().into()); elements } pub fn capture( &self, renderer: &mut GlesRenderer, ) -> anyhow::Result<(Size, Vec)> { let _span = tracy_client::span!("ScreenshotUi::capture"); let Self::Open { selection, output_data, show_pointer, .. } = self else { panic!("screenshot UI must be open to capture"); }; let data = &output_data[&selection.0]; let rect = rect_from_corner_points(selection.1, selection.2); let screenshot = &data.screenshot[0]; // Composite the pointer on top if needed. let mut tex_rect = None; if *show_pointer { if let Some(pointer) = screenshot.pointer.clone() { let scale = pointer.0.buffer().texture_scale(); let offset = rect.loc.upscale(-1); let mut elements = ArrayVec::<_, 2>::new(); elements.push(pointer); elements.push(screenshot.buffer.clone()); let elements = elements.iter().rev().map(|elem| { RelocateRenderElement::from_element(elem, offset, Relocate::Relative) }); let res = render_to_texture( renderer, rect.size, scale, Transform::Normal, Fourcc::Abgr8888, elements, ); match res { Ok((texture, _)) => { tex_rect = Some((texture, Rectangle::from_size(rect.size))); } Err(err) => { warn!("error compositing pointer onto screenshot: {err:?}"); } } } } let (texture, rect) = tex_rect.unwrap_or_else(|| (screenshot.texture.clone(), rect)); // The size doesn't actually matter because we're not transforming anything. let buf_rect = rect .to_logical(1) .to_buffer(1, Transform::Normal, &Size::from((1, 1))); let mapping = renderer .copy_texture(&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: Keysym, mods: ModifiersState) -> Option { let Self::Open { button, .. } = self else { return None; }; // Pressing Space while the button is down goes into origin moving rather than capture. if matches!(button, Button::Down { .. }) && raw == Keysym::space { 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, f64, Transform)> { if let Self::Open { output_data, .. } = self { let data = output_data.get(output)?; Some((data.size, data.scale, data.transform)) } else { None } } /// The pointer has moved to `point` relative to the current selection output. pub fn pointer_motion(&mut self, point: Point, slot: Option) { let Self::Open { selection, output_data, button: Button::Down { touch_slot, on_capture_button, last_pos, move_state, }, .. } = self else { return; }; if *touch_slot != slot { return; } last_pos.1 = point; if *on_capture_button { return; } if let Some(move_state) = move_state { // The cursor offset is relative to selection.1. let delta = point - (selection.1 + move_state.pointer_offset); let desired = rect_from_corner_points(selection.1 + delta, selection.2 + delta); let bounds = Rectangle::from_size(output_data[&selection.0].size - desired.size); let clamped_loc = desired.loc.constrain(bounds); let delta = clamped_loc - rect_from_corner_points(selection.1, selection.2).loc; selection.1 += delta; selection.2 += delta; } else { selection.2 = point; } self.update_buffers(); } pub fn pointer_down( &mut self, output: Output, point: Point, slot: Option, ) -> bool { let Self::Open { selection, output_data, show_pointer, button, .. } = self else { return false; }; // Check if this is a second touch (different slot) while already dragging. if let Some(new_slot) = slot { if let Button::Down { on_capture_button: false, move_state, last_pos, .. } = button { if move_state.is_none() { *move_state = Some(MoveState { pointer_offset: last_pos.1 - selection.1, touch_slot: Some(new_slot), }); } } } if button.is_down() { return false; } let Some(output_data) = output_data.get(&output) else { return false; }; if let Some((show, hide)) = &output_data.panel { let buffer = if *show_pointer { hide } else { show }; let panel_size = buffer.texture().size(); let location = panel_location(output_data, panel_size); if is_within_capture_button(output_data.scale, panel_size, point - location) { *button = Button::Down { touch_slot: slot, on_capture_button: true, last_pos: (output, point), move_state: None, }; return false; } } *button = Button::Down { touch_slot: slot, on_capture_button: false, last_pos: (output.clone(), point), move_state: None, }; *selection = (output, point, point); self.update_buffers(); true } pub fn pointer_up(&mut self, slot: Option) -> Option { let Self::Open { selection, output_data, button, show_pointer, .. } = self else { return None; }; let Button::Down { touch_slot, on_capture_button, ref last_pos, ref mut move_state, .. } = *button else { return None; }; // Check if this is a move touch and if so, stop the move. if let Some(state) = move_state { if state.touch_slot.is_some_and(|m_slot| Some(m_slot) == slot) { *move_state = None; return None; } }; if touch_slot != slot { return None; } let last_pos = last_pos.clone(); *button = Button::Up; // Check if we released still on the capture button. if on_capture_button { let (output, point) = last_pos; #[allow(clippy::question_mark)] let Some(output_data) = output_data.get(&output) else { return None; }; if let Some((show, hide)) = &output_data.panel { let buffer = if *show_pointer { hide } else { show }; let panel_size = buffer.texture().size(); let location = panel_location(output_data, panel_size); if is_within_capture_button(output_data.scale, panel_size, point - location) { return Some(true); } } } // Check if the resulting selection is zero-sized, and try to come up with a small // default rectangle. let (output, a, b) = selection; let mut rect = rect_from_corner_points(*a, *b); if rect.size.is_empty() || rect.size == Size::from((1, 1)) { let data = &output_data[output]; rect = Rectangle::new( Point::from((rect.loc.x - 16, rect.loc.y - 16)), Size::from((32, 32)), ) .intersection(Rectangle::from_size(data.size)) .unwrap_or_default(); *a = rect.loc; *b = rect.loc + rect.size - Size::from((1, 1)); } self.update_buffers(); Some(false) } } impl OutputScreenshot { pub fn from_textures( renderer: &mut GlesRenderer, scale: Scale, texture: GlesTexture, pointer: Option<(GlesTexture, Rectangle)>, ) -> Self { let buffer = PrimaryGpuTextureRenderElement(TextureRenderElement::from_texture_buffer( TextureBuffer::from_texture( renderer, texture.clone(), scale, Transform::Normal, Vec::new(), ), (0., 0.), 1., None, None, Kind::Unspecified, )); let pointer = pointer.map(|(texture, geo)| { PrimaryGpuTextureRenderElement(TextureRenderElement::from_texture_buffer( TextureBuffer::from_texture( renderer, texture, scale, Transform::Normal, Vec::new(), ), geo.to_f64().to_logical(scale).loc, 1., None, None, Kind::Unspecified, )) }); Self { texture, buffer, pointer, } } } fn action(raw: Keysym, mods: ModifiersState) -> Option { if raw == Keysym::Escape { return Some(Action::CancelScreenshot); } if mods.alt || mods.shift { return None; } if !mods.ctrl && (raw == Keysym::space || raw == Keysym::Return) { return Some(Action::ConfirmScreenshot { write_to_disk: true, }); } if mods.ctrl && raw == Keysym::c { return Some(Action::ConfirmScreenshot { write_to_disk: false, }); } if !mods.ctrl && raw == Keysym::p { return Some(Action::ScreenshotTogglePointer); } None } pub fn rect_from_corner_points( a: Point, b: Point, ) -> Rectangle { 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); // We're adding + 1 because the pointer is clamped to output size - 1, so to get the full // screen worth of selection we must add back that + 1. Rectangle::from_extremities((x1, y1), (x2 + 1, y2 + 1)) } fn panel_location(output_data: &OutputData, panel_size: Size) -> Point { let scale = output_data.scale; let padding: i32 = to_physical_precise_round(scale, PADDING); let x = max(0, (output_data.size.w - panel_size.w) / 2); let y = max(0, output_data.size.h - panel_size.h - padding * 2); Point::from((x, y)) } fn is_within_capture_button( scale: f64, panel_size: Size, pos_within_panel: Point, ) -> bool { let padding: i32 = to_physical_precise_round(scale, PADDING); let radius = to_physical_precise_round::(scale, RADIUS) - 2; let xc = padding + radius; let yc = panel_size.h / 2; let pos = pos_within_panel; (pos.x - xc) * (pos.x - xc) + (pos.y - yc) * (pos.y - yc) <= radius * radius } fn render_panel( renderer: &mut GlesRenderer, scale: f64, text: &str, ) -> anyhow::Result> { let _span = tracy_client::span!("screenshot_ui::render_panel"); let padding: i32 = to_physical_precise_round(scale, PADDING); // Keep the border width even to avoid blurry edges. let border_width = (f64::from(BORDER) / 2. * scale).round() * 2.; let half_border_width = (border_width / 2.) as i32; let radius: i32 = to_physical_precise_round(scale, RADIUS); let circle_stroke: f64 = to_physical_precise_round(scale, 2.); // Add 2 px of spacing to separate the backgrounds of the "Space" and "P" keys. let spacing = to_physical_precise_round::(scale, 2) * 1024; let mut font = FontDescription::from_string(FONT); font.set_absolute_size(to_physical_precise_round(scale, font.size())); let surface = ImageSurface::create(cairo::Format::ARgb32, 0, 0)?; let cr = cairo::Context::new(&surface)?; let layout = pangocairo::functions::create_layout(&cr); layout.context().set_round_glyph_positions(false); layout.set_font_description(Some(&font)); layout.set_alignment(Alignment::Left); layout.set_markup(text); layout.set_spacing(spacing); let (mut width, mut height) = layout.pixel_size(); width += padding + radius * 2 + padding - half_border_width + padding; height = max(height, radius * 2); height += padding * 2; let surface = ImageSurface::create(cairo::Format::ARgb32, width, height)?; let cr = cairo::Context::new(&surface)?; cr.set_source_rgb(0.1, 0.1, 0.1); cr.paint()?; let padding = f64::from(padding); let half_border_width = f64::from(half_border_width); let r = f64::from(radius); let yc = f64::from(height / 2); cr.new_sub_path(); cr.arc(padding + r, yc, r, 0., TAU); cr.set_source_rgb(1., 1., 1.); cr.fill()?; cr.new_sub_path(); cr.arc(padding + r, yc, r - circle_stroke, 0., TAU); cr.set_source_rgb(0.1, 0.1, 0.1); cr.fill()?; cr.new_sub_path(); cr.arc(padding + r, yc, r - circle_stroke * 2., 0., TAU); cr.set_source_rgb(1., 1., 1.); cr.fill()?; cr.move_to(padding + r * 2. + padding - half_border_width, padding); let layout = pangocairo::functions::create_layout(&cr); layout.context().set_round_glyph_positions(false); layout.set_font_description(Some(&font)); layout.set_alignment(Alignment::Left); layout.set_markup(text); layout.set_spacing(spacing); cr.set_source_rgb(1., 1., 1.); pangocairo::functions::show_layout(&cr, &layout); cr.move_to(0., 0.); cr.line_to(width.into(), 0.); cr.line_to(width.into(), height.into()); cr.line_to(0., height.into()); cr.line_to(0., 0.); cr.set_source_rgb(0.3, 0.3, 0.3); cr.set_line_width(border_width); cr.stroke()?; drop(cr); let data = surface.take_data().unwrap(); let buffer = TextureBuffer::from_memory( renderer, &data, Fourcc::Argb8888, (width, height), false, scale, Transform::Normal, Vec::new(), )?; Ok(buffer) }