mirror of
https://github.com/niri-wm/niri.git
synced 2026-06-21 02:01:55 +07:00
Add exit confirmation dialog
This commit is contained in:
@@ -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::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::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::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::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_memory(
|
||||
&data,
|
||||
Fourcc::Argb8888,
|
||||
(width, height),
|
||||
scale,
|
||||
Transform::Normal,
|
||||
None,
|
||||
);
|
||||
|
||||
Ok(buffer)
|
||||
}
|
||||
+43
-2
@@ -57,6 +57,13 @@ impl State {
|
||||
let hide_hotkey_overlay =
|
||||
self.niri.hotkey_overlay.is_open() && should_hide_hotkey_overlay(&event);
|
||||
|
||||
let hide_exit_confirm_dialog = self
|
||||
.niri
|
||||
.exit_confirm_dialog
|
||||
.as_ref()
|
||||
.map_or(false, |d| d.is_open())
|
||||
&& should_hide_exit_confirm_dialog(&event);
|
||||
|
||||
use InputEvent::*;
|
||||
match event {
|
||||
DeviceAdded { device } => self.on_device_added(device),
|
||||
@@ -91,6 +98,12 @@ impl State {
|
||||
if hide_hotkey_overlay && self.niri.hotkey_overlay.hide() {
|
||||
self.niri.queue_redraw_all();
|
||||
}
|
||||
|
||||
if let Some(dialog) = &mut self.niri.exit_confirm_dialog {
|
||||
if hide_exit_confirm_dialog && dialog.hide() {
|
||||
self.niri.queue_redraw_all();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn process_libinput_event(&mut self, event: &mut InputEvent<LibinputInputBackend>) {
|
||||
@@ -204,6 +217,13 @@ impl State {
|
||||
let key_code = event.key_code();
|
||||
let modified = keysym.modified_sym();
|
||||
let raw = keysym.raw_latin_sym_or_raw_current_sym();
|
||||
|
||||
if let Some(dialog) = &this.niri.exit_confirm_dialog {
|
||||
if dialog.is_open() && pressed && raw == Some(Keysym::Return) {
|
||||
this.niri.stop_signal.stop();
|
||||
}
|
||||
}
|
||||
|
||||
should_intercept_key(
|
||||
&mut this.niri.suppressed_keys,
|
||||
bindings,
|
||||
@@ -232,8 +252,14 @@ impl State {
|
||||
|
||||
match action {
|
||||
Action::Quit => {
|
||||
info!("quitting because quit bind was pressed");
|
||||
self.niri.stop_signal.stop()
|
||||
if let Some(dialog) = &mut self.niri.exit_confirm_dialog {
|
||||
if dialog.show() {
|
||||
self.niri.queue_redraw_all();
|
||||
}
|
||||
} else {
|
||||
info!("quitting because quit bind was pressed");
|
||||
self.niri.stop_signal.stop()
|
||||
}
|
||||
}
|
||||
Action::ChangeVt(vt) => {
|
||||
self.backend.change_vt(vt);
|
||||
@@ -1401,6 +1427,21 @@ fn should_hide_hotkey_overlay<I: InputBackend>(event: &InputEvent<I>) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
fn should_hide_exit_confirm_dialog<I: InputBackend>(event: &InputEvent<I>) -> bool {
|
||||
match event {
|
||||
InputEvent::Keyboard { event } if event.state() == KeyState::Pressed => true,
|
||||
InputEvent::PointerButton { .. }
|
||||
| InputEvent::PointerAxis { .. }
|
||||
| InputEvent::GestureSwipeBegin { .. }
|
||||
| InputEvent::GesturePinchBegin { .. }
|
||||
| InputEvent::TouchDown { .. }
|
||||
| InputEvent::TouchMotion { .. }
|
||||
| InputEvent::TabletToolTip { .. }
|
||||
| InputEvent::TabletToolButton { .. } => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn allowed_when_locked(action: &Action) -> bool {
|
||||
matches!(
|
||||
action,
|
||||
|
||||
@@ -7,6 +7,7 @@ mod config_error_notification;
|
||||
mod cursor;
|
||||
#[cfg(feature = "dbus")]
|
||||
mod dbus;
|
||||
mod exit_confirm_dialog;
|
||||
mod frame_clock;
|
||||
mod handlers;
|
||||
mod hotkey_overlay;
|
||||
|
||||
+18
-1
@@ -98,6 +98,7 @@ use crate::cursor::{CursorManager, CursorTextureCache, RenderCursor, XCursor};
|
||||
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;
|
||||
@@ -191,6 +192,7 @@ pub struct Niri {
|
||||
pub screenshot_ui: ScreenshotUi,
|
||||
pub config_error_notification: ConfigErrorNotification,
|
||||
pub hotkey_overlay: HotkeyOverlay,
|
||||
pub exit_confirm_dialog: Option<ExitConfirmDialog>,
|
||||
|
||||
#[cfg(feature = "dbus")]
|
||||
pub dbus: Option<crate::dbus::DBusServers>,
|
||||
@@ -867,6 +869,13 @@ impl Niri {
|
||||
let screenshot_ui = ScreenshotUi::new();
|
||||
let config_error_notification = ConfigErrorNotification::new();
|
||||
let hotkey_overlay = HotkeyOverlay::new(config.clone(), backend.mod_key());
|
||||
let exit_confirm_dialog = match ExitConfirmDialog::new() {
|
||||
Ok(x) => Some(x),
|
||||
Err(err) => {
|
||||
warn!("error creating the exit confirm dialog: {err:?}");
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
let socket_source = ListeningSocketSource::new_auto().unwrap();
|
||||
let socket_name = socket_source.socket_name().to_os_string();
|
||||
@@ -973,6 +982,7 @@ impl Niri {
|
||||
screenshot_ui,
|
||||
config_error_notification,
|
||||
hotkey_overlay,
|
||||
exit_confirm_dialog,
|
||||
|
||||
#[cfg(feature = "dbus")]
|
||||
dbus: None,
|
||||
@@ -1807,7 +1817,14 @@ impl Niri {
|
||||
elements = self.pointer_element(renderer, output);
|
||||
}
|
||||
|
||||
// The config error notification too.
|
||||
// Next, the exit confirm dialog.
|
||||
if let Some(dialog) = &self.exit_confirm_dialog {
|
||||
if let Some(element) = dialog.render(renderer, output) {
|
||||
elements.push(element.into());
|
||||
}
|
||||
}
|
||||
|
||||
// Next, the config error notification too.
|
||||
if let Some(element) = self.config_error_notification.render(renderer, output) {
|
||||
elements.push(element.into());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user