diff options
| author | Ivan Molodetskikh <yalterz@gmail.com> | 2024-02-21 10:50:30 +0400 |
|---|---|---|
| committer | Ivan Molodetskikh <yalterz@gmail.com> | 2024-02-21 10:50:30 +0400 |
| commit | d1fe6930a7bc5b78b0f005c2105c92094fc4ec52 (patch) | |
| tree | 724f55cfb29695f74bf1689ea108da4ca63f36fa /src/ui | |
| parent | 9e60b344d06979ade51e49eaa766198e3133b909 (diff) | |
| download | niri-d1fe6930a7bc5b78b0f005c2105c92094fc4ec52.tar.gz niri-d1fe6930a7bc5b78b0f005c2105c92094fc4ec52.tar.bz2 niri-d1fe6930a7bc5b78b0f005c2105c92094fc4ec52.zip | |
Move UI elements into submodule
Diffstat (limited to 'src/ui')
| -rw-r--r-- | src/ui/config_error_notification.rs | 252 | ||||
| -rw-r--r-- | src/ui/exit_confirm_dialog.rs | 162 | ||||
| -rw-r--r-- | src/ui/hotkey_overlay.rs | 451 | ||||
| -rw-r--r-- | src/ui/mod.rs | 4 | ||||
| -rw-r--r-- | src/ui/screenshot_ui.rs | 453 |
5 files changed, 1322 insertions, 0 deletions
diff --git a/src/ui/config_error_notification.rs b/src/ui/config_error_notification.rs new file mode 100644 index 00000000..12d92b85 --- /dev/null +++ b/src/ui/config_error_notification.rs @@ -0,0 +1,252 @@ +use std::cell::RefCell; +use std::collections::HashMap; +use std::path::{Path, PathBuf}; +use std::rc::Rc; +use std::time::Duration; + +use niri_config::Config; +use pangocairo::cairo::{self, ImageSurface}; +use pangocairo::pango::FontDescription; +use smithay::backend::renderer::element::memory::{ + MemoryRenderBuffer, MemoryRenderBufferRenderElement, +}; +use smithay::backend::renderer::element::utils::{Relocate, RelocateRenderElement}; +use smithay::backend::renderer::element::{Element, Kind}; +use smithay::output::Output; +use smithay::reexports::gbm::Format as Fourcc; +use smithay::utils::Transform; + +use crate::animation::Animation; +use crate::render_helpers::renderer::NiriRenderer; + +const TEXT: &str = "Failed to parse the config file. \ + Please run <span face='monospace' bgcolor='#000000'>niri validate</span> \ + to see the errors."; +const PADDING: i32 = 8; +const FONT: &str = "sans 14px"; +const BORDER: i32 = 4; + +pub struct ConfigErrorNotification { + state: State, + buffers: RefCell<HashMap<i32, Option<MemoryRenderBuffer>>>, + + // If set, this is a "Created config at {path}" notification. If unset, this is a config error + // notification. + created_path: Option<PathBuf>, + + config: Rc<RefCell<Config>>, +} + +enum State { + Hidden, + Showing(Animation), + Shown(Duration), + Hiding(Animation), +} + +pub type ConfigErrorNotificationRenderElement<R> = + RelocateRenderElement<MemoryRenderBufferRenderElement<R>>; + +impl ConfigErrorNotification { + pub fn new(config: Rc<RefCell<Config>>) -> Self { + Self { + state: State::Hidden, + buffers: RefCell::new(HashMap::new()), + created_path: None, + config, + } + } + + fn animation(&self, from: f64, to: f64) -> Animation { + let c = self.config.borrow(); + Animation::new( + from, + to, + c.animations.config_notification_open_close, + niri_config::Animation::default_config_notification_open_close(), + ) + } + + pub fn show_created(&mut self, created_path: Option<PathBuf>) { + if self.created_path != created_path { + self.created_path = created_path; + self.buffers.borrow_mut().clear(); + } + + self.state = State::Showing(self.animation(0., 1.)); + } + + pub fn show(&mut self) { + if self.created_path.is_some() { + self.created_path = None; + self.buffers.borrow_mut().clear(); + } + + // Show from scratch even if already showing to bring attention. + self.state = State::Showing(self.animation(0., 1.)); + } + + pub fn hide(&mut self) { + if matches!(self.state, State::Hidden) { + return; + } + + self.state = State::Hiding(self.animation(1., 0.)); + } + + pub fn advance_animations(&mut self, target_presentation_time: Duration) { + match &mut self.state { + State::Hidden => (), + State::Showing(anim) => { + anim.set_current_time(target_presentation_time); + if anim.is_done() { + let duration = if self.created_path.is_some() { + // Make this quite a bit longer because it comes with a monitor modeset + // (can take a while) and an important hotkeys popup diverting the + // attention. + Duration::from_secs(8) + } else { + Duration::from_secs(4) + }; + self.state = State::Shown(target_presentation_time + duration); + } + } + State::Shown(deadline) => { + if target_presentation_time >= *deadline { + self.hide(); + } + } + State::Hiding(anim) => { + anim.set_current_time(target_presentation_time); + if anim.is_done() { + self.state = State::Hidden; + } + } + } + } + + pub fn are_animations_ongoing(&self) -> bool { + !matches!(self.state, State::Hidden) + } + + pub fn render<R: NiriRenderer>( + &self, + renderer: &mut R, + output: &Output, + ) -> Option<ConfigErrorNotificationRenderElement<R>> { + if matches!(self.state, State::Hidden) { + return None; + } + + let scale = output.current_scale().integer_scale(); + let path = self.created_path.as_deref(); + + let mut buffers = self.buffers.borrow_mut(); + let buffer = buffers + .entry(scale) + .or_insert_with_key(move |&scale| render(scale, path).ok()); + let buffer = buffer.as_ref()?; + + let elem = MemoryRenderBufferRenderElement::from_buffer( + renderer, + (0., 0.), + buffer, + Some(0.9), + None, + None, + Kind::Unspecified, + ) + .ok()?; + + let output_transform = output.current_transform(); + let output_mode = output.current_mode().unwrap(); + let output_size = output_transform.transform_size(output_mode.size); + + let buffer_size = elem + .geometry(output.current_scale().fractional_scale().into()) + .size; + + let y_range = buffer_size.h + PADDING * 2 * scale; + + let x = (output_size.w / 2 - buffer_size.w / 2).max(0); + let y = match &self.state { + State::Hidden => unreachable!(), + State::Showing(anim) | State::Hiding(anim) => { + (-buffer_size.h as f64 + anim.value() * y_range as f64).round() as i32 + } + State::Shown(_) => PADDING * 2 * scale, + }; + let elem = RelocateRenderElement::from_element(elem, (x, y), Relocate::Absolute); + + Some(elem) + } +} + +fn render(scale: i32, created_path: Option<&Path>) -> anyhow::Result<MemoryRenderBuffer> { + let _span = tracy_client::span!("config_error_notification::render"); + + let padding = PADDING * scale; + + let mut text = String::from(TEXT); + let mut border_color = (1., 0.3, 0.3); + if let Some(path) = created_path { + text = format!( + "Created a default config file at \ + <span face='monospace' bgcolor='#000000'>{:?}</span>", + path + ); + border_color = (0.5, 1., 0.5); + }; + + let mut font = FontDescription::from_string(FONT); + font.set_absolute_size((font.size() * scale).into()); + + let surface = ImageSurface::create(cairo::Format::ARgb32, 0, 0)?; + let cr = cairo::Context::new(&surface)?; + let layout = pangocairo::functions::create_layout(&cr); + layout.set_font_description(Some(&font)); + layout.set_markup(&text); + + let (mut width, mut height) = layout.pixel_size(); + width += padding * 2; + height += padding * 2; + + // FIXME: fix bug in Smithay that rounds pixel sizes down to scale. + width = (width + scale - 1) / scale * scale; + height = (height + scale - 1) / scale * scale; + + 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()?; + + cr.move_to(padding.into(), padding.into()); + let layout = pangocairo::functions::create_layout(&cr); + layout.set_font_description(Some(&font)); + layout.set_markup(&text); + + 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(border_color.0, border_color.1, border_color.2); + cr.set_line_width((BORDER * scale).into()); + cr.stroke()?; + drop(cr); + + let data = surface.take_data().unwrap(); + let buffer = MemoryRenderBuffer::from_slice( + &data, + Fourcc::Argb8888, + (width, height), + scale, + Transform::Normal, + None, + ); + + Ok(buffer) +} diff --git a/src/ui/exit_confirm_dialog.rs b/src/ui/exit_confirm_dialog.rs new file mode 100644 index 00000000..84eddefe --- /dev/null +++ b/src/ui/exit_confirm_dialog.rs @@ -0,0 +1,162 @@ +use std::cell::RefCell; +use std::collections::HashMap; + +use pangocairo::cairo::{self, ImageSurface}; +use pangocairo::pango::{Alignment, FontDescription}; +use smithay::backend::renderer::element::memory::{ + MemoryRenderBuffer, MemoryRenderBufferRenderElement, +}; +use smithay::backend::renderer::element::utils::{Relocate, RelocateRenderElement}; +use smithay::backend::renderer::element::{Element, Kind}; +use smithay::output::Output; +use smithay::reexports::gbm::Format as Fourcc; +use smithay::utils::Transform; + +use crate::render_helpers::renderer::NiriRenderer; + +const TEXT: &str = "Are you sure you want to exit niri?\n\n\ + Press <span face='mono' bgcolor='#2C2C2C'> Enter </span> to confirm."; +const PADDING: i32 = 16; +const FONT: &str = "sans 14px"; +const BORDER: i32 = 8; + +pub struct ExitConfirmDialog { + is_open: bool, + buffers: RefCell<HashMap<i32, Option<MemoryRenderBuffer>>>, +} + +pub type ExitConfirmDialogRenderElement<R> = + RelocateRenderElement<MemoryRenderBufferRenderElement<R>>; + +impl ExitConfirmDialog { + pub fn new() -> anyhow::Result<Self> { + Ok(Self { + is_open: false, + buffers: RefCell::new(HashMap::from([(1, Some(render(1)?))])), + }) + } + + pub fn show(&mut self) -> bool { + if !self.is_open { + self.is_open = true; + true + } else { + false + } + } + + pub fn hide(&mut self) -> bool { + if self.is_open { + self.is_open = false; + true + } else { + false + } + } + + pub fn is_open(&self) -> bool { + self.is_open + } + + pub fn render<R: NiriRenderer>( + &self, + renderer: &mut R, + output: &Output, + ) -> Option<ExitConfirmDialogRenderElement<R>> { + if !self.is_open { + return None; + } + + let scale = output.current_scale().integer_scale(); + + let mut buffers = self.buffers.borrow_mut(); + let fallback = buffers[&1].clone().unwrap(); + let buffer = buffers.entry(scale).or_insert_with(|| render(scale).ok()); + let buffer = buffer.as_ref().unwrap_or(&fallback); + + let elem = MemoryRenderBufferRenderElement::from_buffer( + renderer, + (0., 0.), + buffer, + None, + None, + None, + Kind::Unspecified, + ) + .ok()?; + + let output_transform = output.current_transform(); + let output_mode = output.current_mode().unwrap(); + let output_size = output_transform.transform_size(output_mode.size); + + let buffer_size = elem + .geometry(output.current_scale().fractional_scale().into()) + .size; + + let x = (output_size.w / 2 - buffer_size.w / 2).max(0); + let y = (output_size.h / 2 - buffer_size.h / 2).max(0); + let elem = RelocateRenderElement::from_element(elem, (x, y), Relocate::Absolute); + + Some(elem) + } +} + +fn render(scale: i32) -> anyhow::Result<MemoryRenderBuffer> { + let _span = tracy_client::span!("exit_confirm_dialog::render"); + + let padding = PADDING * scale; + + let mut font = FontDescription::from_string(FONT); + font.set_absolute_size((font.size() * scale).into()); + + let surface = ImageSurface::create(cairo::Format::ARgb32, 0, 0)?; + let cr = cairo::Context::new(&surface)?; + let layout = pangocairo::functions::create_layout(&cr); + layout.set_font_description(Some(&font)); + layout.set_alignment(Alignment::Center); + layout.set_markup(TEXT); + + let (mut width, mut height) = layout.pixel_size(); + width += padding * 2; + height += padding * 2; + + // FIXME: fix bug in Smithay that rounds pixel sizes down to scale. + width = (width + scale - 1) / scale * scale; + height = (height + scale - 1) / scale * scale; + + 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()?; + + cr.move_to(padding.into(), padding.into()); + let layout = pangocairo::functions::create_layout(&cr); + layout.set_font_description(Some(&font)); + layout.set_alignment(Alignment::Center); + layout.set_markup(TEXT); + + 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(1., 0.3, 0.3); + cr.set_line_width((BORDER * scale).into()); + cr.stroke()?; + drop(cr); + + let data = surface.take_data().unwrap(); + let buffer = MemoryRenderBuffer::from_slice( + &data, + Fourcc::Argb8888, + (width, height), + scale, + Transform::Normal, + None, + ); + + Ok(buffer) +} diff --git a/src/ui/hotkey_overlay.rs b/src/ui/hotkey_overlay.rs new file mode 100644 index 00000000..bfb263f4 --- /dev/null +++ b/src/ui/hotkey_overlay.rs @@ -0,0 +1,451 @@ +use std::cell::RefCell; +use std::cmp::max; +use std::collections::HashMap; +use std::iter::zip; +use std::rc::Rc; + +use niri_config::{Action, Config, Key, Modifiers}; +use pangocairo::cairo::{self, ImageSurface}; +use pangocairo::pango::{AttrColor, AttrInt, AttrList, AttrString, FontDescription, Weight}; +use smithay::backend::renderer::element::memory::{ + MemoryRenderBuffer, MemoryRenderBufferRenderElement, +}; +use smithay::backend::renderer::element::utils::{Relocate, RelocateRenderElement}; +use smithay::backend::renderer::element::Kind; +use smithay::input::keyboard::xkb::keysym_get_name; +use smithay::output::{Output, WeakOutput}; +use smithay::reexports::gbm::Format as Fourcc; +use smithay::utils::{Physical, Size, Transform}; + +use crate::input::CompositorMod; +use crate::render_helpers::renderer::NiriRenderer; + +const PADDING: i32 = 8; +const MARGIN: i32 = PADDING * 2; +const FONT: &str = "sans 14px"; +const BORDER: i32 = 4; +const LINE_INTERVAL: i32 = 2; +const TITLE: &str = "Important Hotkeys"; + +pub struct HotkeyOverlay { + is_open: bool, + config: Rc<RefCell<Config>>, + comp_mod: CompositorMod, + buffers: RefCell<HashMap<WeakOutput, RenderedOverlay>>, +} + +pub struct RenderedOverlay { + buffer: Option<MemoryRenderBuffer>, + size: Size<i32, Physical>, + scale: i32, +} + +pub type HotkeyOverlayRenderElement<R> = RelocateRenderElement<MemoryRenderBufferRenderElement<R>>; + +impl HotkeyOverlay { + pub fn new(config: Rc<RefCell<Config>>, comp_mod: CompositorMod) -> Self { + Self { + is_open: false, + config, + comp_mod, + buffers: RefCell::new(HashMap::new()), + } + } + + pub fn show(&mut self) -> bool { + if !self.is_open { + self.is_open = true; + true + } else { + false + } + } + + pub fn hide(&mut self) -> bool { + if self.is_open { + self.is_open = false; + true + } else { + false + } + } + + pub fn is_open(&self) -> bool { + self.is_open + } + + pub fn on_hotkey_config_updated(&mut self) { + self.buffers.borrow_mut().clear(); + } + + pub fn render<R: NiriRenderer>( + &self, + renderer: &mut R, + output: &Output, + ) -> Option<HotkeyOverlayRenderElement<R>> { + if !self.is_open { + return None; + } + + let scale = output.current_scale().integer_scale(); + let margin = MARGIN * scale; + + let output_transform = output.current_transform(); + let output_mode = output.current_mode().unwrap(); + let output_size = output_transform.transform_size(output_mode.size); + + let mut buffers = self.buffers.borrow_mut(); + buffers.retain(|output, _| output.upgrade().is_some()); + + // FIXME: should probably use the working area rather than view size. + let weak = output.downgrade(); + if let Some(rendered) = buffers.get(&weak) { + if rendered.scale != scale { + buffers.remove(&weak); + } + } + + let rendered = buffers.entry(weak).or_insert_with(|| { + render(&self.config.borrow(), self.comp_mod, scale).unwrap_or_else(|_| { + // This can go negative but whatever, as long as there's no rerender loop. + let mut size = output_size; + size.w -= margin * 2; + size.h -= margin * 2; + RenderedOverlay { + buffer: None, + size, + scale, + } + }) + }); + let buffer = rendered.buffer.as_ref()?; + + let elem = MemoryRenderBufferRenderElement::from_buffer( + renderer, + (0., 0.), + buffer, + Some(0.9), + None, + None, + Kind::Unspecified, + ) + .ok()?; + + let x = (output_size.w / 2 - rendered.size.w / 2).max(0); + let y = (output_size.h / 2 - rendered.size.h / 2).max(0); + let elem = RelocateRenderElement::from_element(elem, (x, y), Relocate::Absolute); + + Some(elem) + } +} + +fn render(config: &Config, comp_mod: CompositorMod, scale: i32) -> anyhow::Result<RenderedOverlay> { + let _span = tracy_client::span!("hotkey_overlay::render"); + + // let margin = MARGIN * scale; + let padding = PADDING * scale; + let line_interval = LINE_INTERVAL * scale; + + // FIXME: if it doesn't fit, try splitting in two columns or something. + // let mut target_size = output_size; + // target_size.w -= margin * 2; + // target_size.h -= margin * 2; + // anyhow::ensure!(target_size.w > 0 && target_size.h > 0); + + let binds = &config.binds.0; + + // Collect actions that we want to show. + let mut actions = vec![&Action::ShowHotkeyOverlay]; + + // Prefer Quit(false) if found, otherwise try Quit(true), and if there's neither, fall back to + // Quit(false). + if binds + .iter() + .any(|bind| bind.actions.first() == Some(&Action::Quit(false))) + { + actions.push(&Action::Quit(false)); + } else if binds + .iter() + .any(|bind| bind.actions.first() == Some(&Action::Quit(true))) + { + actions.push(&Action::Quit(true)); + } else { + actions.push(&Action::Quit(false)); + } + + actions.extend(&[ + &Action::CloseWindow, + &Action::FocusColumnLeft, + &Action::FocusColumnRight, + &Action::MoveColumnLeft, + &Action::MoveColumnRight, + &Action::FocusWorkspaceDown, + &Action::FocusWorkspaceUp, + ]); + + // Prefer move-column-to-workspace-down, but fall back to move-window-to-workspace-down. + if binds + .iter() + .any(|bind| bind.actions.first() == Some(&Action::MoveColumnToWorkspaceDown)) + { + actions.push(&Action::MoveColumnToWorkspaceDown); + } else if binds + .iter() + .any(|bind| bind.actions.first() == Some(&Action::MoveWindowToWorkspaceDown)) + { + actions.push(&Action::MoveWindowToWorkspaceDown); + } else { + actions.push(&Action::MoveColumnToWorkspaceDown); + } + + // Same for -up. + if binds + .iter() + .any(|bind| bind.actions.first() == Some(&Action::MoveColumnToWorkspaceUp)) + { + actions.push(&Action::MoveColumnToWorkspaceUp); + } else if binds + .iter() + .any(|bind| bind.actions.first() == Some(&Action::MoveWindowToWorkspaceUp)) + { + actions.push(&Action::MoveWindowToWorkspaceUp); + } else { + actions.push(&Action::MoveColumnToWorkspaceUp); + } + + actions.extend(&[ + &Action::SwitchPresetColumnWidth, + &Action::MaximizeColumn, + &Action::ConsumeWindowIntoColumn, + &Action::ExpelWindowFromColumn, + ]); + + // Screenshot is not as important, can omit if not bound. + if binds + .iter() + .any(|bind| bind.actions.first() == Some(&Action::Screenshot)) + { + actions.push(&Action::Screenshot); + } + + // Add the spawn actions. + let mut spawn_actions = Vec::new(); + for bind in binds.iter().filter(|bind| { + matches!(bind.actions.first(), Some(Action::Spawn(_))) + // Only show binds with Mod or Super to filter out stuff like volume up/down. + && (bind.key.modifiers.contains(Modifiers::COMPOSITOR) + || bind.key.modifiers.contains(Modifiers::SUPER)) + }) { + let action = bind.actions.first().unwrap(); + + // We only show one bind for each action, so we need to deduplicate the Spawn actions. + if !spawn_actions.contains(&action) { + spawn_actions.push(action); + } + } + actions.extend(spawn_actions); + + let strings = actions + .into_iter() + .map(|action| { + let key = config + .binds + .0 + .iter() + .find(|bind| bind.actions.first() == Some(action)) + .map(|bind| key_name(comp_mod, &bind.key)) + .unwrap_or_else(|| String::from("(not bound)")); + + (format!(" {key} "), action_name(action)) + }) + .collect::<Vec<_>>(); + + let mut font = FontDescription::from_string(FONT); + font.set_absolute_size((font.size() * scale).into()); + + let surface = ImageSurface::create(cairo::Format::ARgb32, 0, 0)?; + let cr = cairo::Context::new(&surface)?; + let layout = pangocairo::functions::create_layout(&cr); + layout.set_font_description(Some(&font)); + + let bold = AttrList::new(); + bold.insert(AttrInt::new_weight(Weight::Bold)); + layout.set_attributes(Some(&bold)); + layout.set_text(TITLE); + let title_size = layout.pixel_size(); + + let attrs = AttrList::new(); + attrs.insert(AttrString::new_family("Monospace")); + attrs.insert(AttrColor::new_background(12000, 12000, 12000)); + + layout.set_attributes(Some(&attrs)); + let key_sizes = strings + .iter() + .map(|(key, _)| { + layout.set_text(key); + layout.pixel_size() + }) + .collect::<Vec<_>>(); + + layout.set_attributes(None); + let action_sizes = strings + .iter() + .map(|(_, action)| { + layout.set_markup(action); + layout.pixel_size() + }) + .collect::<Vec<_>>(); + + let key_width = key_sizes.iter().map(|(w, _)| w).max().unwrap(); + let action_width = action_sizes.iter().map(|(w, _)| w).max().unwrap(); + let mut width = key_width + padding + action_width; + + let mut height = zip(&key_sizes, &action_sizes) + .map(|((_, key_h), (_, act_h))| max(key_h, act_h)) + .sum::<i32>() + + (key_sizes.len() - 1) as i32 * line_interval + + title_size.1 + + padding; + + width += padding * 2; + height += padding * 2; + + // FIXME: fix bug in Smithay that rounds pixel sizes down to scale. + width = (width + scale - 1) / scale * scale; + height = (height + scale - 1) / scale * scale; + + 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()?; + + cr.move_to(padding.into(), padding.into()); + let layout = pangocairo::functions::create_layout(&cr); + layout.set_font_description(Some(&font)); + + cr.set_source_rgb(1., 1., 1.); + + cr.move_to(((width - title_size.0) / 2).into(), padding.into()); + layout.set_attributes(Some(&bold)); + layout.set_text(TITLE); + pangocairo::functions::show_layout(&cr, &layout); + + cr.move_to(padding.into(), (padding + title_size.1 + padding).into()); + + for ((key, action), ((_, key_h), (_, act_h))) in zip(&strings, zip(&key_sizes, &action_sizes)) { + layout.set_attributes(Some(&attrs)); + layout.set_text(key); + pangocairo::functions::show_layout(&cr, &layout); + + cr.rel_move_to((key_width + padding).into(), 0.); + + layout.set_attributes(None); + layout.set_markup(action); + pangocairo::functions::show_layout(&cr, &layout); + + cr.rel_move_to( + (-(key_width + padding)).into(), + (max(key_h, act_h) + line_interval).into(), + ); + } + + 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.5, 0.8, 1.0); + cr.set_line_width((BORDER * scale).into()); + cr.stroke()?; + drop(cr); + + let data = surface.take_data().unwrap(); + let buffer = MemoryRenderBuffer::from_slice( + &data, + Fourcc::Argb8888, + (width, height), + scale, + Transform::Normal, + None, + ); + + Ok(RenderedOverlay { + buffer: Some(buffer), + size: Size::from((width, height)), + scale, + }) +} + +fn action_name(action: &Action) -> String { + match action { + Action::Quit(_) => String::from("Exit niri"), + Action::ShowHotkeyOverlay => String::from("Show Important Hotkeys"), + Action::CloseWindow => String::from("Close Focused Window"), + Action::FocusColumnLeft => String::from("Focus Column to the Left"), + Action::FocusColumnRight => String::from("Focus Column to the Right"), + Action::MoveColumnLeft => String::from("Move Column Left"), + Action::MoveColumnRight => String::from("Move Column Right"), + Action::FocusWorkspaceDown => String::from("Switch Workspace Down"), + Action::FocusWorkspaceUp => String::from("Switch Workspace Up"), + Action::MoveColumnToWorkspaceDown => String::from("Move Column to Workspace Down"), + Action::MoveColumnToWorkspaceUp => String::from("Move Column to Workspace Up"), + Action::MoveWindowToWorkspaceDown => String::from("Move Window to Workspace Down"), + Action::MoveWindowToWorkspaceUp => String::from("Move Window to Workspace Up"), + Action::SwitchPresetColumnWidth => String::from("Switch Preset Column Widths"), + Action::MaximizeColumn => String::from("Maximize Column"), + Action::ConsumeWindowIntoColumn => String::from("Consume Window Into Column"), + Action::ExpelWindowFromColumn => String::from("Expel Window From Column"), + Action::Screenshot => String::from("Take a Screenshot"), + Action::Spawn(args) => format!( + "Spawn <span face='monospace' bgcolor='#000000'>{}</span>", + args.first().unwrap_or(&String::new()) + ), + _ => String::from("FIXME: Unknown"), + } +} + +fn key_name(comp_mod: CompositorMod, key: &Key) -> String { + let mut name = String::new(); + + let has_comp_mod = key.modifiers.contains(Modifiers::COMPOSITOR); + + if key.modifiers.contains(Modifiers::SUPER) + || (has_comp_mod && comp_mod == CompositorMod::Super) + { + name.push_str("Super + "); + } + if key.modifiers.contains(Modifiers::ALT) || (has_comp_mod && comp_mod == CompositorMod::Alt) { + name.push_str("Alt + "); + } + if key.modifiers.contains(Modifiers::SHIFT) { + name.push_str("Shift + "); + } + if key.modifiers.contains(Modifiers::CTRL) { + name.push_str("Ctrl + "); + } + name.push_str(&prettify_keysym_name(&keysym_get_name(key.keysym))); + + name +} + +fn prettify_keysym_name(name: &str) -> String { + let name = match name { + "slash" => "/", + "comma" => ",", + "period" => ".", + "minus" => "-", + "equal" => "=", + "grave" => "`", + "Next" => "Page Down", + "Prior" => "Page Up", + "Print" => "PrtSc", + "Return" => "Enter", + _ => name, + }; + + if name.len() == 1 && name.is_ascii() { + name.to_ascii_uppercase() + } else { + name.into() + } +} diff --git a/src/ui/mod.rs b/src/ui/mod.rs new file mode 100644 index 00000000..a63c6f03 --- /dev/null +++ b/src/ui/mod.rs @@ -0,0 +1,4 @@ +pub mod config_error_notification; +pub mod exit_confirm_dialog; +pub mod hotkey_overlay; +pub mod screenshot_ui; diff --git a/src/ui/screenshot_ui.rs b/src/ui/screenshot_ui.rs new file mode 100644 index 00000000..897761a1 --- /dev/null +++ b/src/ui/screenshot_ui.rs @@ -0,0 +1,453 @@ +use std::cmp::{max, min}; +use std::collections::HashMap; +use std::iter::zip; +use std::mem; + +use anyhow::Context; +use arrayvec::ArrayVec; +use niri_config::Action; +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::utils::{Physical, Point, Rectangle, Size, Transform}; + +use crate::niri_render_elements; +use crate::render_helpers::primary_gpu_texture::PrimaryGpuTextureRenderElement; + +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>, + scale: i32, + transform: Transform, + texture: GlesTexture, + texture_buffer: TextureBuffer<GlesTexture>, + buffers: [SolidColorBuffer; 8], + locations: [Point<i32, Physical>; 8], +} + +niri_render_elements! { + ScreenshotUiRenderElement => { + Screenshot = PrimaryGpuTextureRenderElement, + 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 transform = output.current_transform(); + let output_mode = output.current_mode().unwrap(); + let size = transform.transform_size(output_mode.size); + let scale = output.current_scale().integer_scale(); + let texture_buffer = TextureBuffer::from_texture( + renderer, + texture.clone(), + scale, + Transform::Normal, + |
