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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### Added

- About dialog: a "Report a problem" row. It copies your diagnostics to the clipboard and opens `https://wayscriber.com/report`, which hands them straight into a prefilled bug form. Nothing is sent automatically, and the diagnostics travel in the URL fragment, so they never reach a server log.
- Automatic guidance toasts now provide explicit "Got it" and "Tip settings…" controls. The General UI setting can disable automatic tips without disabling manual tours or real warnings.

### Breaking (Rust source)

Expand All @@ -14,3 +15,5 @@
### Fixed

- Stylus pressure no longer overrides the selected Marker/Textmarker or Step Marker size. Pressure-to-thickness mapping remains limited to pressure-sensitive freehand Pen strokes.
- Automatic tips and skipped-default shortcut notices now remember acknowledgement and taught-feature use instead of returning every active launch. Persistence failures are surfaced instead of causing repeat loops.
- First-run onboarding no longer requires the radial-menu flick-to-commit exercise. Saved sessions paused on that retired step continue at the reference step.
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -576,6 +576,17 @@ Once the overlay is up:
- <kbd>F11</kbd> — [configurator](#configurator-gui)
- <kbd>Escape</kbd> — hide or exit

Discovery and shortcut-coaching tips have **Got it** and **Tip settings…**
controls. **Got it**
permanently acknowledges only that tip; **Tip settings…** does the same and
opens the Configurator at the global automatic-guidance switch. Using the
feature a tip teaches also stops that tip from returning. Clicking the message
body dismisses it for the current run, and unattended tips stop after three
appearances. The toolbar-hidden recovery tip keeps **Show** as its primary
control and offers the same settings route. Turning automatic guidance off
applies to later overlay launches; manual tour replay and real capability or
configuration warnings remain available.

For daily use, set up [daemon mode](#daemon-mode-recommended): toggles are faster and session state survives between activations.

---
Expand Down
5 changes: 3 additions & 2 deletions docs/CONFIG.md
Original file line number Diff line number Diff line change
Expand Up @@ -708,7 +708,8 @@ help_overlay_context_filter = true
show_capabilities_warning = true

# Show automatic first-run guidance, discovery tips, and shortcut coaching.
# The guided tour remains available manually when this is false.
# Automatic tips can also be acknowledged individually, and stop after three
# appearances. The guided tour remains available manually when this is false.
show_onboarding_hints = true

# Command palette action toast duration (ms)
Expand Down Expand Up @@ -1177,7 +1178,7 @@ top_controls = [
- **Polygon tools**: Full mode shows Triangle, Parallelogram, Rhombus, Regular Polygon, and Freeform Polygon under the compact Polygons picker. Simple mode exposes them in the Shapes picker.
- **Context-aware UI**: `context_aware_ui` shows/hides tool-specific controls (colors, thickness, arrow labels, etc.) based on the active tool; disable to always show all controls.
- **Preset toasts**: `show_preset_toasts` enables toast confirmations for preset apply/save/clear.
- **Automatic guidance**: `show_onboarding_hints` controls first-run cards, discovery tips, and shortcut coaching. Set it to `false` to disable all automatic tutorials; the guided tour remains available manually. Completed profiles migrated from onboarding versions before v6 are not enrolled in the later status-bar, Canvas, and zoom tip series.
- **Automatic guidance**: `show_onboarding_hints` controls first-run cards, discovery tips, and shortcut coaching. Discovery and coaching tips offer **Got it** (permanently acknowledge that tip) and **Tip settings…** (acknowledge it, then open the Configurator at this setting); the toolbar-hidden recovery tip keeps **Show** as its primary control and offers the same settings route. Using the board picker, bottom-right zoom controls, or Canvas popover also acknowledges the matching tip. Clicking the message body dismisses a tip only for the current run; an unattended tip stops after three appearances. Set this option to `false` to disable all automatic tutorials on later overlay launches; the running overlay does not live-reload this Configurator change. The guided tour remains available manually, and capability, safety, and configuration warnings are unaffected. Completed profiles migrated from onboarding versions before v6 are not enrolled in the later status-bar, Canvas, and zoom tip series. If onboarding progress cannot be saved, automatic guidance is disabled for that run and an actionable persistence warning is shown.
- **Capability warnings**: `show_capabilities_warning` independently controls compositor limitation warnings; disabling tutorials does not hide safety, configuration, or capability diagnostics.
- **Tool preview**: `show_tool_preview` toggles the cursor bubble.
- **Offsets**: `top_offset` and `top_offset_y` are the authored default top-toolbar position. Dragging the strip saves its position as a runtime preference in `runtime-ui.toml` and leaves these untouched; editing one here again takes over from the saved drag.
Expand Down
4 changes: 2 additions & 2 deletions src/backend/wayland/handlers/pointer/release.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,8 @@ impl WaylandState {
screen_x.round() as i32,
screen_y.round() as i32,
);
if hit && let Some(action) = action {
self.dispatch_input_action(action);
if hit && let Some(command) = action {
self.handle_toast_command(command);
}
}
return;
Expand Down
4 changes: 2 additions & 2 deletions src/backend/wayland/handlers/touch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -520,8 +520,8 @@ impl WaylandState {
let (hit, action) = self
.input_state
.resolve_toast_release(pressed, screen_x, screen_y);
if hit && let Some(action) = action {
self.dispatch_input_action(action);
if hit && let Some(command) = action {
self.handle_toast_command(command);
}
return;
}
Expand Down
174 changes: 126 additions & 48 deletions src/backend/wayland/state/onboarding.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use crate::config::keybindings::Action;
use crate::input::state::{Toast, ToastPriority};
use crate::domain::OnboardingTip;
use crate::input::state::{Toast, ToastCommand, ToastPriority};
use crate::onboarding::DEFERRED_HINT_REPEAT_MAX;
use std::time::{Duration, Instant};

Expand All @@ -15,6 +16,30 @@ const SHORTCUT_COACH_COOLDOWN: Duration = Duration::from_secs(90);
/// Maximum coach hints shown per session.
const SHORTCUT_COACH_SESSION_CAP: u32 = 2;

enum ContextualHint {
Help,
CommandPalette,
QuickAccess,
StatusBar(&'static str),
CanvasPopover,
ZoomChip,
}

struct TipAcknowledgementOutcome {
follow_up: Option<Action>,
persistence_error: Option<crate::onboarding::OnboardingSaveError>,
}

fn acknowledge_tip_command(
result: Result<(), crate::onboarding::OnboardingSaveError>,
follow_up: Option<Action>,
) -> TipAcknowledgementOutcome {
TipAcknowledgementOutcome {
follow_up,
persistence_error: result.err(),
}
}

/// The visible status-HUD entry that can open the board picker. The hint must
/// not advertise the pill when both configurable Board/Page segments are
/// absent: the remaining color/tool/help segments perform different actions.
Expand All @@ -40,6 +65,18 @@ fn canvas_popover_hint_relevant(input: &crate::input::InputState) -> bool {
&& input.toolbar_top_display_mode == crate::config::TopDisplayMode::Full
}

fn automatic_tip_toast(message: impl Into<String>, tip: OnboardingTip) -> Toast {
Toast::info(message)
.command("Got it", ToastCommand::AcknowledgeTip { tip, then: None })
.secondary_command(
"Tip settings…",
ToastCommand::AcknowledgeTip {
tip,
then: Some(Action::OpenConfiguratorOnboardingHints),
},
)
}

/// Per-session shortcut-coach accumulator. Session-only (never persisted): the
/// across-session cap and learned-suppression live in `OnboardingState`.
#[derive(Debug, Default)]
Expand Down Expand Up @@ -106,6 +143,21 @@ pub(super) fn shortcut_coach_should_fire(
}

impl WaylandState {
pub(in crate::backend::wayland) fn handle_toast_command(&mut self, command: ToastCommand) {
match command {
ToastCommand::Dispatch(action) => self.dispatch_input_action(action),
ToastCommand::AcknowledgeTip { tip, then } => {
let outcome = acknowledge_tip_command(self.onboarding.acknowledge_tip(tip), then);
if let Some(action) = outcome.follow_up {
self.dispatch_input_action(action);
}
if let Some(error) = outcome.persistence_error {
self.show_onboarding_persistence_warning(&error);
}
}
}
}

pub(in crate::backend::wayland) fn apply_onboarding_hints(&mut self) {
// Show capability warning toast first if applicable.
self.apply_capability_toast();
Expand Down Expand Up @@ -200,7 +252,7 @@ impl WaylandState {
let outcome = self.input_state.push_toast(
ToastPriority::Hint,
"onboarding.coach",
Toast::info(message).action("Tip settings…", Action::OpenConfiguratorOnboardingHints),
automatic_tip_toast(message, OnboardingTip::ShortcutCoach),
);
if outcome.accepted() {
let session = &mut self.data.shortcut_coach;
Expand Down Expand Up @@ -250,8 +302,7 @@ impl WaylandState {
let canvas_hint_relevant = canvas_popover_hint_relevant(&self.input_state);

let mut changed = false;
let mut hint_kind: Option<&'static str> = None;
let mut status_bar_hint_entry: Option<&str> = None;
let mut hint = None;
{
let state = self.onboarding.state_mut();
if !state.first_run_completed {
Expand All @@ -265,7 +316,7 @@ impl WaylandState {
state.hint_help_shown = true;
state.hint_help_count = state.hint_help_count.saturating_add(1);
changed = true;
hint_kind = Some("help");
hint = Some(ContextualHint::Help);
} else if state.sessions_seen >= 3
&& !state.used_command_palette
&& !state.hint_palette_shown
Expand All @@ -274,7 +325,7 @@ impl WaylandState {
state.hint_palette_shown = true;
state.hint_palette_count = state.hint_palette_count.saturating_add(1);
changed = true;
hint_kind = Some("palette");
hint = Some(ContextualHint::CommandPalette);
} else if state.sessions_seen >= 2
&& !state.used_radial_menu
&& !state.used_context_menu_right_click
Expand All @@ -285,7 +336,7 @@ impl WaylandState {
state.hint_quick_access_shown = true;
state.hint_quick_access_count = state.hint_quick_access_count.saturating_add(1);
changed = true;
hint_kind = Some("quick_access");
hint = Some(ContextualHint::QuickAccess);
// M9 surface hints, staggered across sessions so they never all
// fire at once (the else-if chain already limits it to one per tick,
// and the no-active-toast gate keeps them from clobbering). The
Expand All @@ -294,84 +345,97 @@ impl WaylandState {
// the earliest threshold among the new surfaces.
} else if let Some(entry) = status_bar_entry
&& state.sessions_seen >= 3
&& !state.used_board_picker
&& !state.hint_status_bar_shown
&& state.hint_status_bar_count < DEFERRED_HINT_REPEAT_MAX
{
state.hint_status_bar_shown = true;
state.hint_status_bar_count = state.hint_status_bar_count.saturating_add(1);
changed = true;
hint_kind = Some("status_bar");
status_bar_hint_entry = Some(entry);
hint = Some(ContextualHint::StatusBar(entry));
} else if canvas_hint_relevant
&& state.sessions_seen >= 5
&& !state.used_canvas_popover
&& !state.hint_canvas_popover_shown
&& state.hint_canvas_popover_count < DEFERRED_HINT_REPEAT_MAX
{
state.hint_canvas_popover_shown = true;
state.hint_canvas_popover_count = state.hint_canvas_popover_count.saturating_add(1);
changed = true;
hint_kind = Some("canvas_popover");
hint = Some(ContextualHint::CanvasPopover);
} else if zoom_chip_present
&& state.sessions_seen >= 7
&& !state.used_zoom_control
&& !state.hint_zoom_chip_shown
&& state.hint_zoom_chip_count < DEFERRED_HINT_REPEAT_MAX
{
state.hint_zoom_chip_shown = true;
state.hint_zoom_chip_count = state.hint_zoom_chip_count.saturating_add(1);
changed = true;
hint_kind = Some("zoom_chip");
hint = Some(ContextualHint::ZoomChip);
}
}

if changed && !self.save_onboarding_state() {
hint_kind = None;
hint = None;
}
if let Some(kind) = hint_kind {
let message = match kind {
"help" => format!(
"Press {} for all shortcuts.",
self.shortcut_label(Action::ToggleHelp, "Help")
if let Some(hint) = hint {
let (tip, message) = match hint {
ContextualHint::Help => (
OnboardingTip::Help,
format!(
"Press {} for all shortcuts.",
self.shortcut_label(Action::ToggleHelp, "Help")
),
),
"palette" => format!(
"Press {} to search actions.",
self.shortcut_label(Action::ToggleCommandPalette, "Command Palette")
ContextualHint::CommandPalette => (
OnboardingTip::CommandPalette,
format!(
"Press {} to search actions.",
self.shortcut_label(Action::ToggleCommandPalette, "Command Palette")
),
),
"status_bar" => {
let entry = status_bar_hint_entry
.expect("status_bar hint is only queued when the picker is on screen");
ContextualHint::StatusBar(entry) => (
OnboardingTip::StatusBar,
format!(
"Click the {entry} segment in the status bar to switch boards and pages."
)
}
"canvas_popover" => {
),
),
ContextualHint::CanvasPopover => (
OnboardingTip::CanvasPopover,
"Open \u{201c}Canvas\u{2026}\u{201d} from the \u{2026} overflow for boards, \
pages, zoom, and advanced controls."
.to_string()
}
"zoom_chip" => match self.shortcut_label_opt(Action::ZoomIn) {
Some(key) => {
format!("Zoom from the chip in the bottom-right corner, or press {key}.")
}
None => "Zoom from the chip in the bottom-right corner.".to_string(),
},
_ => {
.to_string(),
),
ContextualHint::ZoomChip => (
OnboardingTip::ZoomChip,
match self.shortcut_label_opt(Action::ZoomIn) {
Some(key) => {
format!(
"Zoom from the chip in the bottom-right corner, or press {key}."
)
}
None => "Zoom from the chip in the bottom-right corner.".to_string(),
},
),
ContextualHint::QuickAccess => {
let context = self.shortcut_label_opt(Action::OpenContextMenu);
let radial = self.shortcut_label_opt(Action::ToggleRadialMenu);
match (context, radial) {
let message = match (context, radial) {
(Some(c), Some(r)) => format!("Try quick access: {c} or {r}."),
(Some(c), None) => format!("Try quick access: {c}."),
(None, Some(r)) => format!("Try quick access: {r}."),
(None, None) => {
"Quick-access menus are available from toolbar actions.".to_string()
}
}
};
(OnboardingTip::QuickAccess, message)
}
};
self.input_state.push_toast(
ToastPriority::Hint,
"onboarding.hint",
Toast::info(message)
.action("Tip settings…", Action::OpenConfiguratorOnboardingHints),
automatic_tip_toast(message, tip),
);
}
}
Expand All @@ -398,7 +462,14 @@ impl WaylandState {
ToastPriority::Hint,
"onboarding.toolbar",
Toast::info("Toolbars hidden")
.action(format!("Show ({toolbar_binding})"), Action::ToggleToolbar),
.action(format!("Show ({toolbar_binding})"), Action::ToggleToolbar)
.secondary_command(
"Tip settings…",
ToastCommand::AcknowledgeTip {
tip: OnboardingTip::ToolbarHidden,
then: Some(Action::OpenConfiguratorOnboardingHints),
},
),
);
if outcome.accepted() {
self.onboarding.state_mut().toolbar_hint_shown = true;
Expand All @@ -415,19 +486,26 @@ impl WaylandState {
match self.onboarding.save() {
Ok(()) => true,
Err(error) => {
self.input_state.push_toast(
ToastPriority::Critical,
"onboarding.persistence",
Toast::warning(format!(
"Automatic guidance is off because onboarding progress could not be saved: {error}"
))
.once_per_content(),
);
self.show_onboarding_persistence_warning(&error);
false
}
}
}

fn show_onboarding_persistence_warning(
&mut self,
error: &crate::onboarding::OnboardingSaveError,
) {
self.input_state.push_toast(
ToastPriority::Critical,
"onboarding.persistence",
Toast::warning(format!(
"Automatic guidance is off because onboarding progress could not be saved: {error}"
))
.once_per_content(),
);
}

fn shortcut_label_opt(&self, action: Action) -> Option<String> {
self.input_state.shortcut_for_action(action)
}
Expand Down
Loading
Loading