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
12 changes: 4 additions & 8 deletions src/backend/wayland/backend/state_init/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ use super::tray::process_tray_action;
use crate::{
capture::CaptureManager,
config::Config,
input::state::UI_TOAST_DURATION_MS,
input::{BoardMode, InputState, UiToastKind},
input::{BoardMode, InputState},
onboarding::OnboardingStore,
};

Expand Down Expand Up @@ -43,13 +42,10 @@ pub(super) fn init_state(backend: &WaylandBackend, setup: WaylandSetup) -> Resul
let mut input_state = input_state::build_input_state(&config);
let mut onboarding = OnboardingStore::load();
if !onboarding.state().welcome_shown {
input_state.toggle_help_overlay();
input_state.set_ui_toast_with_duration(
UiToastKind::Info,
"Welcome! F1 help, F2/F9 toolbars, 1-5 presets, F11 configurator, Esc exits.",
UI_TOAST_DURATION_MS.saturating_mul(3),
);
// Start the guided tour for new users
input_state.start_tour();
onboarding.state_mut().welcome_shown = true;
onboarding.state_mut().tour_shown = true;
onboarding.save();
}
apply_initial_mode(backend, &config, &mut input_state);
Expand Down
4 changes: 4 additions & 0 deletions src/backend/wayland/handlers/pointer/motion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,10 @@ impl WaylandState {
return;
}
self.set_current_mouse(event.position.0 as i32, event.position.1 as i32);
// Block pointer motion when tour overlay is active
if self.input_state.tour_active {
return;
}
let (wx, wy) = self.zoomed_world_coords(event.position.0, event.position.1);
self.input_state.update_pointer_position(wx, wy);
self.input_state.on_mouse_motion(wx, wy);
Expand Down
5 changes: 5 additions & 0 deletions src/backend/wayland/handlers/pointer/press.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ impl WaylandState {
inline_active: bool,
button: u32,
) {
// Block pointer input when tour overlay is active
if self.input_state.tour_active {
return;
}

if debug_toolbar_drag_logging_enabled() {
debug!(
"pointer press: button={}, on_toolbar={}, inline_active={}, drag_active={}",
Expand Down
13 changes: 10 additions & 3 deletions src/backend/wayland/handlers/pointer/release.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ impl WaylandState {
inline_active: bool,
button: u32,
) {
// Block pointer input when tour overlay is active
if self.input_state.tour_active {
return;
}

if debug_toolbar_drag_logging_enabled() {
debug!(
"pointer release: button={}, on_toolbar={}, inline_active={}, drag_active={}, toolbar_dragging={}, pointer_over_toolbar={}",
Expand Down Expand Up @@ -71,9 +76,11 @@ impl WaylandState {
if mb == MouseButton::Left {
let screen_x = event.position.0 as i32;
let screen_y = event.position.1 as i32;
if let Some(action) = self.input_state.check_toast_click(screen_x, screen_y) {
self.input_state.handle_action(action);
self.input_state.needs_redraw = true;
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;
}
}
Expand Down
3 changes: 3 additions & 0 deletions src/backend/wayland/state/render/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,9 @@ impl WaylandState {
}
self.render_inline_toolbars(ctx, &snapshot);
}

// Tour overlay renders last (on top of everything including toolbars)
crate::ui::render_tour(ctx, &self.input_state, width, height);
} else {
self.input_state.clear_context_menu_layout();
}
Expand Down
3 changes: 3 additions & 0 deletions src/config/keybindings/actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,4 +123,7 @@ pub enum Action {
ClearPreset3,
ClearPreset4,
ClearPreset5,

// Onboarding
ReplayTour,
}
3 changes: 1 addition & 2 deletions src/input/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@ pub mod tool;
pub use board_mode::BoardMode;
pub use events::{Key, MouseButton};
pub use state::{
ClickHighlightSettings, DrawingState, InputState, TextInputMode, ToolbarDrawerTab, UiToastKind,
ZoomAction,
ClickHighlightSettings, DrawingState, InputState, TextInputMode, ToolbarDrawerTab, ZoomAction,
};
#[cfg(tablet)]
#[allow(unused_imports)]
Expand Down
4 changes: 4 additions & 0 deletions src/input/state/actions/action_ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ impl InputState {
self.open_capture_folder();
true
}
Action::ReplayTour => {
self.start_tour();
true
}
_ => false,
}
}
Expand Down
5 changes: 5 additions & 0 deletions src/input/state/actions/key_press/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ impl InputState {
/// - Help toggle (configurable)
/// - Modifier key tracking
pub fn on_key_press(&mut self, key: Key) {
// Tour takes highest priority when active
if self.tour_active && self.handle_tour_key(key) {
return;
}

if self.show_help && self.handle_help_overlay_key(key) {
return;
}
Expand Down
2 changes: 2 additions & 0 deletions src/input/state/core/base/state/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,8 @@ impl InputState {
active_preset_slot: None,
preset_feedback: vec![None; PRESET_SLOTS_MAX],
pending_preset_action: None,
tour_active: false,
tour_step: 0,
};

if state.click_highlight.uses_pen_color() {
Expand Down
4 changes: 4 additions & 0 deletions src/input/state/core/base/state/structs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -246,4 +246,8 @@ pub struct InputState {
pub(crate) preset_feedback: Vec<Option<PresetFeedbackState>>,
/// Pending preset save/clear action for backend persistence
pub(in crate::input::state::core) pending_preset_action: Option<PresetAction>,
/// Whether the guided tour is currently active
pub tour_active: bool,
/// Current step in the guided tour (0-indexed)
pub tour_step: usize,
}
2 changes: 2 additions & 0 deletions src/input/state/core/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ mod properties;
mod selection;
mod selection_actions;
mod tool_controls;
mod tour;
mod utility;

pub(crate) use base::TextClickState;
Expand All @@ -20,3 +21,4 @@ pub use base::{
#[allow(unused_imports)]
pub use menus::{ContextMenuEntry, ContextMenuKind, ContextMenuState, MenuCommand};
pub use selection::SelectionState;
pub use tour::TourStep;
154 changes: 154 additions & 0 deletions src/input/state/core/tour.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
//! Guided tour system for onboarding new users.

use crate::input::events::Key;

use super::base::InputState;

/// Tour step definitions.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TourStep {
Welcome,
DrawingBasics,
ToolbarIntro,
HelpOverlay,
Presets,
Complete,
}

impl TourStep {
/// Total number of tour steps.
pub const COUNT: usize = 6;

/// Get step from index.
pub fn from_index(index: usize) -> Option<Self> {
match index {
0 => Some(Self::Welcome),
1 => Some(Self::DrawingBasics),
2 => Some(Self::ToolbarIntro),
3 => Some(Self::HelpOverlay),
4 => Some(Self::Presets),
5 => Some(Self::Complete),
_ => None,
}
}

/// Get step title.
pub fn title(&self) -> &'static str {
match self {
Self::Welcome => "Welcome to Wayscriber",
Self::DrawingBasics => "Drawing Basics",
Self::ToolbarIntro => "Toolbar Access",
Self::HelpOverlay => "Help & Shortcuts",
Self::Presets => "Quick Presets",
Self::Complete => "Tour Complete",
}
}

/// Get step description.
pub fn description(&self) -> &'static str {
match self {
Self::Welcome => {
"Wayscriber is a screen annotation tool.\nDraw anywhere on your screen to highlight, explain, or present."
}
Self::DrawingBasics => {
"Click and drag to draw with the pen tool.\nUse R/G/B/Y keys to change colors.\nScroll wheel or +/- to adjust thickness."
}
Self::ToolbarIntro => {
"Press F2 to toggle the toolbar.\nThe toolbar provides quick access to all tools and settings."
}
Self::HelpOverlay => {
"Press F1 to see all keyboard shortcuts.\nType to search for specific commands."
}
Self::Presets => {
"Keys 1-5 apply saved tool presets.\nShift+1-5 saves current tool settings.\nCtrl+1-5 clears a preset slot."
}
Self::Complete => {
"You're ready to annotate!\nPress F1 anytime to review shortcuts.\nEnjoy using Wayscriber!"
}
}
}

/// Get navigation hint for the step.
pub fn nav_hint(&self) -> &'static str {
match self {
Self::Complete => "Press Enter or Escape to finish",
_ => "Space/Enter: Next | Backspace: Back | Escape: Skip",
}
}
}

impl InputState {
/// Start the guided tour.
pub fn start_tour(&mut self) {
self.tour_active = true;
self.tour_step = 0;
// Close other overlays
if self.show_help {
self.show_help = false;
}
self.close_context_menu();
self.close_properties_panel();
self.dirty_tracker.mark_full();
self.needs_redraw = true;
}

/// End the tour (skip or complete).
pub fn end_tour(&mut self) {
self.tour_active = false;
self.dirty_tracker.mark_full();
self.needs_redraw = true;
}

/// Advance to the next tour step.
pub fn tour_next(&mut self) {
if self.tour_step + 1 < TourStep::COUNT {
self.tour_step += 1;
self.dirty_tracker.mark_full();
self.needs_redraw = true;
} else {
self.end_tour();
}
}

/// Go back to the previous tour step.
pub fn tour_prev(&mut self) {
if self.tour_step > 0 {
self.tour_step -= 1;
self.dirty_tracker.mark_full();
self.needs_redraw = true;
}
}

/// Get the current tour step.
pub fn current_tour_step(&self) -> Option<TourStep> {
if self.tour_active {
TourStep::from_index(self.tour_step)
} else {
None
}
}

/// Handle a key press while the tour is active.
/// Returns true if the key was handled.
pub(crate) fn handle_tour_key(&mut self, key: Key) -> bool {
if !self.tour_active {
return false;
}

match key {
Key::Escape => {
self.end_tour();
true
}
Key::Return | Key::Space => {
self.tour_next();
true
}
Key::Backspace => {
self.tour_prev();
true
}
_ => true, // Consume all other keys while tour is active
}
}
}
30 changes: 16 additions & 14 deletions src/input/state/core/utility/toasts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,28 +91,30 @@ impl InputState {
true
}

/// Check if a click at (x, y) hits the toast. If so and the toast has an action,
/// returns the action and dismisses the toast.
/// 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) -> Option<Action> {
let bounds = self.ui_toast_bounds?;
let toast = self.ui_toast.as_ref()?;
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
if let Some(ref action) = toast.action {
let result = action.action;
// Dismiss the toast
self.ui_toast = None;
self.ui_toast_bounds = None;
self.needs_redraw = true;
return Some(result);
}
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);
}
None
(false, None)
}
}
2 changes: 1 addition & 1 deletion src/input/state/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@ pub use core::{
ContextMenuEntry, ContextMenuKind, ContextMenuState, DrawingState, InputState,
MAX_STROKE_THICKNESS, MIN_STROKE_THICKNESS, PRESET_FEEDBACK_DURATION_MS,
PRESET_TOAST_DURATION_MS, PresetAction, PresetFeedbackKind, SelectionAxis, SelectionState,
TextInputMode, ToolbarDrawerTab, UI_TOAST_DURATION_MS, UiToastKind, ZoomAction,
TextInputMode, ToolbarDrawerTab, TourStep, UI_TOAST_DURATION_MS, UiToastKind, ZoomAction,
};
pub use highlight::ClickHighlightSettings;
Loading