From d1fe6930a7bc5b78b0f005c2105c92094fc4ec52 Mon Sep 17 00:00:00 2001 From: Ivan Molodetskikh Date: Wed, 21 Feb 2024 10:50:30 +0400 Subject: Move UI elements into submodule --- src/config_error_notification.rs | 252 -------------------- src/exit_confirm_dialog.rs | 162 ------------- src/hotkey_overlay.rs | 451 ----------------------------------- src/input.rs | 2 +- src/lib.rs | 5 +- src/niri.rs | 8 +- src/screenshot_ui.rs | 453 ------------------------------------ src/ui/config_error_notification.rs | 252 ++++++++++++++++++++ src/ui/exit_confirm_dialog.rs | 162 +++++++++++++ src/ui/hotkey_overlay.rs | 451 +++++++++++++++++++++++++++++++++++ src/ui/mod.rs | 4 + src/ui/screenshot_ui.rs | 453 ++++++++++++++++++++++++++++++++++++ 12 files changed, 1328 insertions(+), 1327 deletions(-) delete mode 100644 src/config_error_notification.rs delete mode 100644 src/exit_confirm_dialog.rs delete mode 100644 src/hotkey_overlay.rs delete mode 100644 src/screenshot_ui.rs create mode 100644 src/ui/config_error_notification.rs create mode 100644 src/ui/exit_confirm_dialog.rs create mode 100644 src/ui/hotkey_overlay.rs create mode 100644 src/ui/mod.rs create mode 100644 src/ui/screenshot_ui.rs diff --git a/src/config_error_notification.rs b/src/config_error_notification.rs deleted file mode 100644 index 12d92b85..00000000 --- a/src/config_error_notification.rs +++ /dev/null @@ -1,252 +0,0 @@ -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 niri validate \ - to see the errors."; -const PADDING: i32 = 8; -const FONT: &str = "sans 14px"; -const BORDER: i32 = 4; - -pub struct ConfigErrorNotification { - state: State, - buffers: RefCell>>, - - // If set, this is a "Created config at {path}" notification. If unset, this is a config error - // notification. - created_path: Option, - - config: Rc>, -} - -enum State { - Hidden, - Showing(Animation), - Shown(Duration), - Hiding(Animation), -} - -pub type ConfigErrorNotificationRenderElement = - RelocateRenderElement>; - -impl ConfigErrorNotification { - pub fn new(config: Rc>) -> 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) { - 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( - &self, - renderer: &mut R, - output: &Output, - ) -> Option> { - 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 { - 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 \ - {:?}", - 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/exit_confirm_dialog.rs b/src/exit_confirm_dialog.rs deleted file mode 100644 index 84eddefe..00000000 --- a/src/exit_confirm_dialog.rs +++ /dev/null @@ -1,162 +0,0 @@ -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 Enter to confirm."; -const PADDING: i32 = 16; -const FONT: &str = "sans 14px"; -const BORDER: i32 = 8; - -pub struct ExitConfirmDialog { - is_open: bool, - buffers: RefCell>>, -} - -pub type ExitConfirmDialogRenderElement = - RelocateRenderElement>; - -impl ExitConfirmDialog { - pub fn new() -> anyhow::Result { - 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( - &self, - renderer: &mut R, - output: &Output, - ) -> Option> { - 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 { - 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/hotkey_overlay.rs b/src/hotkey_overlay.rs deleted file mode 100644 index bfb263f4..00000000 --- a/src/hotkey_overlay.rs +++ /dev/null @@ -1,451 +0,0 @@ -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>, - comp_mod: CompositorMod, - buffers: RefCell>, -} - -pub struct RenderedOverlay { - buffer: Option, - size: Size, - scale: i32, -} - -pub type HotkeyOverlayRenderElement = RelocateRenderElement>; - -impl HotkeyOverlay { - pub fn new(config: Rc>, 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( - &self, - renderer: &mut R, - output: &Output, - ) -> Option> { - 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 { - 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::>(); - - 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::>(); - - layout.set_attributes(None); - let action_sizes = strings - .iter() - .map(|(_, action)| { - layout.set_markup(action); - layout.pixel_size() - }) - .collect::>(); - - 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::() - + (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 {}", - 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/input.rs b/src/input.rs index 11651d2f..0d14e7e6 100644 --- a/src/input.rs +++ b/src/input.rs @@ -23,7 +23,7 @@ use smithay::wayland::pointer_constraints::{with_pointer_constraint, PointerCons use smithay::wayland::tablet_manager::{TabletDescriptor, TabletSeatTrait}; use crate::niri::State; -use crate::screenshot_ui::ScreenshotUi; +use crate::ui::screenshot_ui::ScreenshotUi; use crate::utils::spawning::spawn; use crate::utils::{center, get_monotonic_time}; diff --git a/src/lib.rs b/src/lib.rs index 68bcbc36..26f1277b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,21 +4,18 @@ extern crate tracing; pub mod animation; pub mod backend; pub mod cli; -pub mod config_error_notification; pub mod cursor; #[cfg(feature = "dbus")] pub mod dbus; -pub mod exit_confirm_dialog; pub mod frame_clock; pub mod handlers; -pub mod hotkey_overlay; pub mod input; pub mod ipc; pub mod layout; pub mod niri; pub mod protocols; pub mod render_helpers; -pub mod screenshot_ui; +pub mod ui; pub mod utils; #[cfg(not(feature = "xdp-gnome-screencast"))] diff --git a/src/niri.rs b/src/niri.rs index d82a978e..69ae6dec 100644 --- a/src/niri.rs +++ b/src/niri.rs @@ -87,16 +87,13 @@ use smithay::wayland::virtual_keyboard::VirtualKeyboardManagerState; use crate::backend::tty::SurfaceDmabufFeedback; use crate::backend::{Backend, RenderResult, Tty, Winit}; -use crate::config_error_notification::ConfigErrorNotification; use crate::cursor::{CursorManager, CursorTextureCache, RenderCursor, XCursor}; #[cfg(feature = "dbus")] use crate::dbus::gnome_shell_screenshot::{NiriToScreenshot, ScreenshotToNiri}; #[cfg(feature = "xdp-gnome-screencast")] use crate::dbus::mutter_screen_cast::{self, ScreenCastToNiri}; -use crate::exit_confirm_dialog::ExitConfirmDialog; use crate::frame_clock::FrameClock; use crate::handlers::configure_lock_surface; -use crate::hotkey_overlay::HotkeyOverlay; use crate::input::{apply_libinput_settings, TabletData}; use crate::ipc::server::IpcServer; use crate::layout::{Layout, MonitorRenderElement}; @@ -104,7 +101,10 @@ use crate::protocols::foreign_toplevel::{self, ForeignToplevelManagerState}; use crate::pw_utils::{Cast, PipeWire}; use crate::render_helpers::renderer::NiriRenderer; use crate::render_helpers::{render_to_texture, render_to_vec}; -use crate::screenshot_ui::{ScreenshotUi, ScreenshotUiRenderElement}; +use crate::ui::config_error_notification::ConfigErrorNotification; +use crate::ui::exit_confirm_dialog::ExitConfirmDialog; +use crate::ui::hotkey_overlay::HotkeyOverlay; +use crate::ui::screenshot_ui::{ScreenshotUi, ScreenshotUiRenderElement}; use crate::utils::{ center, get_monotonic_time, make_screenshot_path, output_size, write_png_rgba8, }; diff --git a/src/screenshot_ui.rs b/src/screenshot_ui.rs deleted file mode 100644 index 897761a1..00000000 --- a/src/screenshot_ui.rs +++ /dev/null @@ -1,453 +0,0 @@ -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)>, - }, - Open { - selection: (Output, Point, Point), - output_data: HashMap, - mouse_down: bool, - }, -} - -pub struct OutputData { - size: Size, - scale: i32, - transform: Transform, - texture: GlesTexture, - texture_buffer: TextureBuffer, - buffers: [SolidColorBuffer; 8], - locations: [Point; 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, - 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, - 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, - scale, - transform, - 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 { - 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( - PrimaryGpuTextureRenderElement(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, Vec)> { - 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, mods: ModifiersState) -> Option { - 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, 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) { - 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, - 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 down && !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 { - 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, - b: Point, - scale: i32, -) -> 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); - Rectangle::from_extemities((x1, y1), (x2 + scale, y2 + scale)) -} 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 niri validate \ + to see the errors."; +const PADDING: i32 = 8; +const FONT: &str = "sans 14px"; +const BORDER: i32 = 4; + +pub struct ConfigErrorNotification { + state: State, + buffers: RefCell>>, + + // If set, this is a "Created config at {path}" notification. If unset, this is a config error + // notification. + created_path: Option, + + config: Rc>, +} + +enum State { + Hidden, + Showing(Animation), + Shown(Duration), + Hiding(Animation), +} + +pub type ConfigErrorNotificationRenderElement = + RelocateRenderElement>; + +impl ConfigErrorNotification { + pub fn new(config: Rc>) -> 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) { + 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( + &self, + renderer: &mut R, + output: &Output, + ) -> Option> { + 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 { + 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 \ + {:?}", + 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 Enter to confirm."; +const PADDING: i32 = 16; +const FONT: &str = "sans 14px"; +const BORDER: i32 = 8; + +pub struct ExitConfirmDialog { + is_open: bool, + buffers: RefCell>>, +} + +pub type ExitConfirmDialogRenderElement = + RelocateRenderElement>; + +impl ExitConfirmDialog { + pub fn new() -> anyhow::Result { + 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( + &self, + renderer: &mut R, + output: &Output, + ) -> Option> { + 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 { + 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>, + comp_mod: CompositorMod, + buffers: RefCell>, +} + +pub struct RenderedOverlay { + buffer: Option, + size: Size, + scale: i32, +} + +pub type HotkeyOverlayRenderElement = RelocateRenderElement>; + +impl HotkeyOverlay { + pub fn new(config: Rc>, 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( + &self, + renderer: &mut R, + output: &Output, + ) -> Option> { + 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); + } + } + +