Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions src/backend/wayland/handlers/keyboard/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ impl KeyboardHandler for WaylandState {
} else {
self.clear_toolbar_focus();
}
// Mark overlay as ready once we have focus and surface is configured
if self.surface.is_configured() {
self.set_overlay_ready(true);
debug!("Overlay ready for keybinds");
}
}

fn leave(
Expand All @@ -45,6 +50,7 @@ impl KeyboardHandler for WaylandState {
) {
debug!("Keyboard focus left");
self.set_keyboard_focus(false);
self.set_overlay_ready(false);
self.clear_toolbar_focus();

// When the compositor moves focus away from our surface (e.g. to a portal
Expand Down Expand Up @@ -74,6 +80,11 @@ impl KeyboardHandler for WaylandState {
_serial: u32,
event: KeyEvent,
) {
// Block keybinds until overlay is fully ready (prevents Ctrl+W leaking to apps)
if !self.is_overlay_ready() {
debug!("Ignoring key press before overlay ready");
return;
}
let key = keysym_to_key(event.keysym);
if self.zoom.is_engaged() {
match key {
Expand Down Expand Up @@ -172,6 +183,10 @@ impl KeyboardHandler for WaylandState {
_serial: u32,
event: KeyEvent,
) {
// Block keybinds until overlay is fully ready
if !self.is_overlay_ready() {
return;
}
let key = keysym_to_key(event.keysym);
if self.zoom.active {
match key {
Expand Down
6 changes: 6 additions & 0 deletions src/backend/wayland/handlers/layer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,12 @@ impl LayerShellHandler for WaylandState {
self.surface.set_configured(true);
self.input_state.needs_redraw = true;

// Mark overlay ready if we already have keyboard focus (configure came after enter)
if self.has_keyboard_focus() && !self.is_overlay_ready() {
self.set_overlay_ready(true);
debug!("Overlay ready for keybinds (from configure)");
}

let (phys_w, phys_h) = self.surface.physical_dimensions();
self.frozen
.handle_resize(phys_w, phys_h, &mut self.input_state);
Expand Down
13 changes: 13 additions & 0 deletions src/backend/wayland/handlers/pointer/release.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,19 @@ impl WaylandState {
_ => return,
};

// Check for toast click before other handling (toast uses screen coords)
if mb == MouseButton::Left {
let screen_x = event.position.0 as i32;
let screen_y = event.position.1 as i32;
let (hit, action) = self.input_state.check_toast_click(screen_x, screen_y);
if hit {
if let Some(action) = action {
self.input_state.handle_action(action);
}
return;
}
}

let (wx, wy) = self.zoomed_world_coords(event.position.0, event.position.1);
self.input_state.on_mouse_release(mb, wx, wy);
self.input_state.needs_redraw = true;
Expand Down
7 changes: 7 additions & 0 deletions src/backend/wayland/handlers/xdg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,13 @@ impl WindowHandler for WaylandState {
}

self.surface.set_configured(true);

// Mark overlay ready if we already have keyboard focus (configure came after enter)
if self.has_keyboard_focus() && !self.is_overlay_ready() {
self.set_overlay_ready(true);
log::debug!("Overlay ready for keybinds (from xdg configure)");
}

self.input_state
.update_screen_dimensions(self.surface.width(), self.surface.height());
let (phys_w, phys_h) = self.surface.physical_dimensions();
Expand Down
10 changes: 10 additions & 0 deletions src/backend/wayland/state/core/accessors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,4 +127,14 @@ impl WaylandState {
) -> Option<&mut SessionOptions> {
self.session.options_mut()
}

/// Returns true if the overlay is ready to process keybinds (surface configured + focus).
pub(in crate::backend::wayland) fn is_overlay_ready(&self) -> bool {
self.data.overlay_ready
}

/// Sets the overlay ready state. Should be true only when surface is configured and has focus.
pub(in crate::backend::wayland) fn set_overlay_ready(&mut self, value: bool) {
self.data.overlay_ready = value;
}
}
3 changes: 3 additions & 0 deletions src/backend/wayland/state/data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ pub struct StateData {
pub(super) preferred_output_identity: Option<String>,
pub(super) xdg_fullscreen: bool,
pub(super) overlay_suppression: OverlaySuppression,
/// True when surface is configured and has keyboard focus; keys are blocked until ready.
pub(super) overlay_ready: bool,
}

impl StateData {
Expand Down Expand Up @@ -115,6 +117,7 @@ impl StateData {
preferred_output_identity: None,
xdg_fullscreen: false,
overlay_suppression: OverlaySuppression::None,
overlay_ready: false,
}
}
}
9 changes: 7 additions & 2 deletions src/backend/wayland/state/onboarding.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::config::keybindings::Action;
use crate::input::state::UiToastKind;

use super::*;
Expand All @@ -17,8 +18,12 @@ impl WaylandState {
return;
}

self.input_state
.set_ui_toast(UiToastKind::Info, "Toolbars hidden. Press F2/F9 to show.");
self.input_state.set_ui_toast_with_action(
UiToastKind::Info,
"Toolbars hidden",
"Show (F2)",
Action::ToggleToolbar,
);
self.onboarding.state_mut().toolbar_hint_shown = true;
self.onboarding.save();
}
Expand Down
3 changes: 2 additions & 1 deletion src/backend/wayland/state/render/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,8 @@ impl WaylandState {
self.input_state.help_overlay_scroll.clamp(0.0, scroll_max);
}

crate::ui::render_ui_toast(ctx, &self.input_state, width, height);
self.input_state.ui_toast_bounds =
crate::ui::render_ui_toast(ctx, &self.input_state, width, height);
crate::ui::render_preset_toast(ctx, &self.input_state, width, height);

if !self.zoom.active {
Expand Down
8 changes: 2 additions & 6 deletions src/backend/wayland/toolbar/layout/side/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ pub(super) fn push_settings_hits(ctx: &SideLayoutContext<'_>, y: f64, hits: &mut
let toggle_gap = ToolbarLayoutSpec::SIDE_TOGGLE_GAP;
let mut toggles: Vec<(ToolbarEvent, Option<&str>)> = vec![
(
ToolbarEvent::ToggleToolPreview(!ctx.snapshot.show_tool_preview),
Some("Tool preview: cursor bubble."),
ToolbarEvent::ToggleTextControls(!ctx.snapshot.show_text_controls),
Some("Text: font size/family."),
),
(
ToolbarEvent::ToggleStatusBar(!ctx.snapshot.show_status_bar),
Expand Down Expand Up @@ -53,10 +53,6 @@ pub(super) fn push_settings_hits(ctx: &SideLayoutContext<'_>, y: f64, hits: &mut
ToolbarEvent::ToggleStepSection(!ctx.snapshot.show_step_section),
Some("Step: step undo/redo."),
),
(
ToolbarEvent::ToggleTextControls(!ctx.snapshot.show_text_controls),
Some("Text: font size/family."),
),
]);
}

Expand Down
4 changes: 2 additions & 2 deletions src/backend/wayland/toolbar/layout/spec/side/sizes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -262,9 +262,9 @@ impl ToolbarLayoutSpec {
) -> f64 {
let toggle_h = Self::SIDE_TOGGLE_HEIGHT;
let toggle_gap = Self::SIDE_TOGGLE_GAP;
let mut toggle_count = 3; // Tool preview + status bar + preset toasts
let mut toggle_count = 3; // Text controls + status bar + preset toasts
if snapshot.layout_mode != ToolbarLayoutMode::Simple {
toggle_count += 7; // presets, actions, zoom actions, advanced actions, pages, step section, text controls
toggle_count += 6; // presets, actions, zoom actions, advanced actions, pages, step section
}
let rows = (toggle_count + 1) / 2;
let toggle_rows_h = if rows > 0 {
Expand Down
14 changes: 4 additions & 10 deletions src/backend/wayland/toolbar/render/side_palette/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,10 @@ pub(super) fn draw_settings_section(layout: &mut SidePaletteLayout, y: &mut f64)
let toggle_gap = ToolbarLayoutSpec::SIDE_TOGGLE_GAP;
let mut toggles: Vec<(&str, bool, ToolbarEvent, Option<&str>)> = vec![
(
"Tool preview",
snapshot.show_tool_preview,
ToolbarEvent::ToggleToolPreview(!snapshot.show_tool_preview),
Some("Tool preview: cursor bubble."),
"Text controls",
snapshot.show_text_controls,
ToolbarEvent::ToggleTextControls(!snapshot.show_text_controls),
Some("Text: font size/family."),
),
(
"Status bar",
Expand Down Expand Up @@ -97,12 +97,6 @@ pub(super) fn draw_settings_section(layout: &mut SidePaletteLayout, y: &mut f64)
ToolbarEvent::ToggleStepSection(!snapshot.show_step_section),
Some("Step: step undo/redo."),
),
(
"Text controls",
snapshot.show_text_controls,
ToolbarEvent::ToggleTextControls(!snapshot.show_text_controls),
Some("Text: font size/family."),
),
]);
}

Expand Down
14 changes: 13 additions & 1 deletion src/input/state/actions/help_overlay.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,19 @@ impl InputState {
let search_active = !self.help_overlay_search.trim().is_empty();

match key {
Key::Escape | Key::F1 | Key::F10 => {
Key::Escape => {
// Escape clears search first, then closes overlay
if search_active {
self.help_overlay_search.clear();
self.help_overlay_scroll = 0.0;
self.dirty_tracker.mark_full();
self.needs_redraw = true;
} else {
self.toggle_help_overlay();
}
true
}
Key::F1 | Key::F10 => {
self.toggle_help_overlay();
true
}
Expand Down
2 changes: 1 addition & 1 deletion src/input/state/core/base/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ mod types;
pub use state::InputState;
pub(crate) use state::PresenterRestore;
pub(crate) use types::{
DelayedHistory, HistoryMode, PresetFeedbackState, TextClickState, UiToastState,
DelayedHistory, HistoryMode, PresetFeedbackState, TextClickState, ToastAction, UiToastState,
};
pub use types::{
DrawingState, MAX_STROKE_THICKNESS, MIN_STROKE_THICKNESS, PRESET_FEEDBACK_DURATION_MS,
Expand Down
1 change: 1 addition & 0 deletions src/input/state/core/base/state/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ impl InputState {
show_preset_toasts: true,
show_tool_preview: false,
ui_toast: None,
ui_toast_bounds: None,
selection_clipboard: None,
clipboard_paste_offset: 0,
last_capture_path: None,
Expand Down
2 changes: 2 additions & 0 deletions src/input/state/core/base/state/structs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,8 @@ pub struct InputState {
pub show_tool_preview: bool,
/// Pending UI toast (errors/warnings/info)
pub(crate) ui_toast: Option<UiToastState>,
/// Cached bounds of the rendered toast for click detection (x, y, w, h)
pub(crate) ui_toast_bounds: Option<(f64, f64, f64, f64)>,
/// Copied selection shapes for paste operations
pub(in crate::input::state::core) selection_clipboard: Option<Vec<Shape>>,
/// Offset applied to successive paste operations
Expand Down
10 changes: 10 additions & 0 deletions src/input/state/core/base/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,14 @@ pub enum UiToastKind {
Error,
}

/// Action that can be triggered by clicking a toast.
#[derive(Debug, Clone)]
pub struct ToastAction {
pub label: String,
#[allow(dead_code)] // Used in check_toast_click via WaylandState
pub action: crate::config::keybindings::Action,
}

#[derive(Debug, Clone)]
pub(crate) struct PresetFeedbackState {
pub kind: PresetFeedbackKind,
Expand All @@ -158,6 +166,8 @@ pub(crate) struct UiToastState {
pub message: String,
pub started: Instant,
pub duration_ms: u64,
/// Optional action that triggers when the toast is clicked.
pub action: Option<ToastAction>,
}

#[derive(Debug, Clone, Copy)]
Expand Down
8 changes: 7 additions & 1 deletion src/input/state/core/utility/presenter_mode.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use super::super::base::{InputState, PresenterRestore, UiToastKind};
use crate::config::keybindings::Action;
use crate::input::tool::Tool;

impl InputState {
Expand Down Expand Up @@ -87,7 +88,12 @@ impl InputState {
self.presenter_restore = Some(restore);
self.presenter_mode = true;
if config.show_toast {
self.set_ui_toast(UiToastKind::Info, "Starting Presenter Mode");
self.set_ui_toast_with_action(
UiToastKind::Info,
"Presenter Mode active",
"Exit",
Action::TogglePresenterMode,
);
}
self.dirty_tracker.mark_full();
self.needs_redraw = true;
Expand Down
55 changes: 54 additions & 1 deletion src/input/state/core/utility/toasts.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
use super::super::base::{InputState, UI_TOAST_DURATION_MS, UiToastKind, UiToastState};
use super::super::base::{
InputState, ToastAction, UI_TOAST_DURATION_MS, UiToastKind, UiToastState,
};
use crate::config::keybindings::Action;
use std::path::Path;
use std::time::{Duration, Instant};

Expand All @@ -18,6 +21,28 @@ impl InputState {
message: message.into(),
started: Instant::now(),
duration_ms,
action: None,
});
self.needs_redraw = true;
}

/// Set a toast with a clickable action. Clicking the toast triggers the action.
pub(crate) fn set_ui_toast_with_action(
&mut self,
kind: UiToastKind,
message: impl Into<String>,
action_label: impl Into<String>,
action: Action,
) {
self.ui_toast = Some(UiToastState {
kind,
message: message.into(),
started: Instant::now(),
duration_ms: UI_TOAST_DURATION_MS,
action: Some(ToastAction {
label: action_label.into(),
action,
}),
});
self.needs_redraw = true;
}
Expand Down Expand Up @@ -60,8 +85,36 @@ impl InputState {
let duration = Duration::from_millis(toast.duration_ms);
if now.saturating_duration_since(toast.started) >= duration {
self.ui_toast = None;
self.ui_toast_bounds = None;
return false;
}
true
}

/// Check if a click at (x, y) hits the toast. If so, dismisses it and returns
/// whether it was hit plus any associated action.
#[allow(dead_code)] // Called from WaylandState pointer release handler
pub(crate) fn check_toast_click(&mut self, x: i32, y: i32) -> (bool, Option<Action>) {
let Some(bounds) = self.ui_toast_bounds else {
return (false, None);
};
let Some(toast) = self.ui_toast.as_ref() else {
return (false, None);
};

// Check if click is within toast bounds
let (bx, by, bw, bh) = bounds;
let xf = x as f64;
let yf = y as f64;
if xf >= bx && xf <= bx + bw && yf >= by && yf <= by + bh {
// Click is within toast
let action = toast.action.as_ref().map(|action| action.action);
// Dismiss the toast
self.ui_toast = None;
self.ui_toast_bounds = None;
self.needs_redraw = true;
return (true, action);
}
(false, None)
}
}
Loading