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
27 changes: 27 additions & 0 deletions config.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,21 @@ pressure_enabled = true
min_thickness = 1.0
max_thickness = 8.0

# Automatically switch to eraser when physical eraser is detected
auto_eraser_switch = true

# Threshold (in pixels) before saving a stroke as pressure-sensitive
pressure_variation_threshold = 0.1

# How thickness edits apply to pressure strokes: disabled, add, scale
pressure_thickness_edit_mode = "disabled"

# When to show thickness entry for pressure strokes: never, pressure_only, any_pressure
pressure_thickness_entry_mode = "pressure_only"

# Per-step scale factor when using scale mode (0.1 = +/-10%)
pressure_thickness_scale_step = 0.1

# ═══════════════════════════════════════════════════════════════════════════════
# CAPTURE SETTINGS
# ═══════════════════════════════════════════════════════════════════════════════
Expand Down Expand Up @@ -569,6 +584,18 @@ persist_history = true
# Set to false if you always want to use the values above (e.g., arrow head placement).
restore_tool_state = true

# Autosave session data while the overlay is running
autosave_enabled = true

# Save after this much idle time following a change (ms)
autosave_idle_ms = 5000

# Maximum interval between saves while dirty (ms)
autosave_interval_ms = 45000

# Backoff before retrying autosave after a failure (ms)
autosave_failure_backoff_ms = 5000

# Storage location: "auto" (XDG data dir), "config" (next to config), or "custom"
storage = "auto"

Expand Down
30 changes: 23 additions & 7 deletions src/backend/wayland/backend/event_loop/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use log::{info, warn};
use std::sync::atomic::Ordering;
use std::time::Instant;
use std::time::{Duration, Instant};
use wayland_client::{Connection, EventQueue};

use super::super::state::WaylandState;
Expand All @@ -16,6 +16,15 @@ pub(super) struct EventLoopOutcome {
pub(super) loop_error: Option<anyhow::Error>,
}

fn min_timeout(a: Option<Duration>, b: Option<Duration>) -> Option<Duration> {
match (a, b) {
(Some(a), Some(b)) => Some(a.min(b)),
(Some(a), None) => Some(a),
(None, Some(b)) => Some(b),
(None, None) => None,
}
}

pub(super) fn run_event_loop(
conn: &Connection,
event_queue: &mut EventQueue<WaylandState>,
Expand Down Expand Up @@ -72,9 +81,11 @@ pub(super) fn run_event_loop(
let should_block = capture_active
|| !state.surface.is_configured()
|| (vsync_enabled && frame_callback_pending);
let animation_timeout = state.ui_animation_timeout(Instant::now());
let now = Instant::now();
let animation_timeout = state.ui_animation_timeout(now);
let autosave_timeout = session_save::autosave_timeout(state, now);
let timeout = if should_block {
None
autosave_timeout
} else if !vsync_enabled && state.input_state.needs_redraw {
// When VSync is off and we need to redraw, wake up when frame budget allows
let frame_cap_timeout = render::frame_rate_cap_timeout(
Expand All @@ -83,13 +94,14 @@ pub(super) fn run_event_loop(
);
// Use the shorter of frame cap timeout and animation timeout.
// If unlimited FPS (None) and no animation, use zero to avoid blocking.
match (frame_cap_timeout, animation_timeout) {
let merged = match (frame_cap_timeout, animation_timeout) {
(Some(fc), Some(anim)) => Some(fc.min(anim)),
(Some(fc), None) => Some(fc),
(None, _) => Some(std::time::Duration::ZERO),
}
(None, _) => Some(Duration::ZERO),
};
min_timeout(merged, autosave_timeout)
} else {
animation_timeout
min_timeout(animation_timeout, autosave_timeout)
};

if let Err(e) = dispatch::dispatch_events(event_queue, state, capture_active, timeout) {
Expand Down Expand Up @@ -126,6 +138,10 @@ pub(super) fn run_event_loop(
capture::handle_pending_actions(state);
state.apply_onboarding_hints();

if let Err(err) = session_save::autosave_if_due(state, Instant::now()) {
warn!("Failed to autosave session state: {}", err);
}

if let Some(err) = render::maybe_render(
state,
qh,
Expand Down
77 changes: 73 additions & 4 deletions src/backend/wayland/backend/event_loop/session_save.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,84 @@
use super::super::super::state::WaylandState;
use crate::{notification, session};
use std::time::{Duration, Instant};

pub(super) fn persist_session(state: &WaylandState) -> Result<(), anyhow::Error> {
if let Some(options) = state.session_options()
&& let Some(snapshot) = session::snapshot_from_input(&state.input_state, options)
{
session::save_snapshot(&snapshot, options)?;
let Some(options) = state.session_options() else {
return Ok(());
};

let snapshot = session::snapshot_from_input(&state.input_state, options);
let _ = save_snapshot_or_clear(state, options, snapshot)?;
Ok(())
}

pub(super) fn autosave_timeout(state: &WaylandState, now: Instant) -> Option<Duration> {
let options = state.session_options()?;
state.session.autosave_timeout(now, options)
}

pub(super) fn autosave_if_due(state: &mut WaylandState, now: Instant) -> Result<(), anyhow::Error> {
let Some(options) = state.session_options().cloned() else {
return Ok(());
};

let input_dirty = state.input_state.take_session_dirty();
state.session.record_input_dirty(now, input_dirty);

if !state.session.autosave_due(now, &options) {
return Ok(());
}

match save_snapshot_or_clear(
state,
&options,
session::snapshot_from_input(&state.input_state, &options),
) {
Ok(saved) => {
if saved {
state.session.mark_saved(now);
}
}
Err(err) => {
if state
.session
.mark_autosave_failure(now, options.autosave_failure_backoff)
{
notify_session_failure(state, &err);
}
return Err(err);
}
}
Ok(())
}

fn save_snapshot_or_clear(
state: &WaylandState,
options: &session::SessionOptions,
snapshot: Option<session::SessionSnapshot>,
) -> Result<bool, anyhow::Error> {
if let Some(snapshot) = snapshot {
session::save_snapshot(&snapshot, options)?;
return Ok(true);
}

if !persistence_enabled(options) {
return Ok(false);
}

let empty_snapshot = session::SessionSnapshot {
active_board_id: state.input_state.board_id().to_string(),
boards: Vec::new(),
tool_state: None,
};
session::save_snapshot(&empty_snapshot, options)?;
Ok(true)
}

fn persistence_enabled(options: &session::SessionOptions) -> bool {
options.any_enabled() || options.restore_tool_state || options.persist_history
}

pub(super) fn notify_session_failure(state: &WaylandState, err: &anyhow::Error) {
notification::send_notification_async(
&state.tokio_handle,
Expand Down
7 changes: 7 additions & 0 deletions src/backend/wayland/backend/state_init/input_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,13 @@ pub(super) fn build_input_state(config: &Config) -> InputState {
input_state.show_status_board_badge = config.ui.show_status_board_badge;
input_state.show_status_page_badge = config.ui.show_status_page_badge;
input_state.show_floating_badge_always = config.ui.show_floating_badge_always;
#[cfg(tablet)]
{
input_state.pressure_variation_threshold = config.tablet.pressure_variation_threshold;
input_state.pressure_thickness_edit_mode = config.tablet.pressure_thickness_edit_mode;
input_state.pressure_thickness_entry_mode = config.tablet.pressure_thickness_entry_mode;
input_state.pressure_thickness_scale_step = config.tablet.pressure_thickness_scale_step;
}

input_state.init_toolbar_from_config(
config.ui.toolbar.layout_mode,
Expand Down
6 changes: 5 additions & 1 deletion src/backend/wayland/backend/state_init/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ pub(super) fn build_session_options(

if let Some(ref opts) = session_options {
info!(
"Session persistence: base_dir={}, per_output={}, display_id='{}', output_identity={:?}, boards[T/W/B]={}/{}/{}, history={}, max_persisted_history={:?}, restore_tool_state={}, max_file_size={} bytes, compression={:?}",
"Session persistence: base_dir={}, per_output={}, display_id='{}', output_identity={:?}, boards[T/W/B]={}/{}/{}, history={}, max_persisted_history={:?}, restore_tool_state={}, autosave_enabled={}, autosave_idle_ms={}, autosave_interval_ms={}, autosave_failure_backoff_ms={}, max_file_size={} bytes, compression={:?}",
opts.base_dir.display(),
opts.per_output,
opts.display_id,
Expand All @@ -65,6 +65,10 @@ pub(super) fn build_session_options(
opts.persist_history,
opts.max_persisted_undo_depth,
opts.restore_tool_state,
opts.autosave_enabled,
opts.autosave_idle.as_millis(),
opts.autosave_interval.as_millis(),
opts.autosave_failure_backoff.as_millis(),
opts.max_file_size_bytes,
opts.compression
);
Expand Down
30 changes: 14 additions & 16 deletions src/backend/wayland/handlers/tablet/tool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,22 +40,20 @@ impl Dispatch<ZwpTabletToolV2, ()> for WaylandState {
state.stylus_last_pos = None;

// Auto-switch to eraser if physical tool is eraser (and config enables it)
if state.config.tablet.auto_eraser_switch {
if let Some(tool_type) = tool_type {
if tool_type.is_eraser() {
// Only auto-switch if not already on eraser
if state.input_state.active_tool() != Tool::Eraser {
// Save the current tool override before switching
state.stylus_pre_eraser_tool_override =
state.input_state.tool_override();
state.input_state.set_tool_override(Some(Tool::Eraser));
state.stylus_auto_switched_to_eraser = true;
info!(
"Auto-switched to eraser (physical eraser detected), saved previous: {:?}",
state.stylus_pre_eraser_tool_override
);
}
}
if state.config.tablet.auto_eraser_switch
&& let Some(tool_type) = tool_type
&& tool_type.is_eraser()
{
// Only auto-switch if not already on eraser
if state.input_state.active_tool() != Tool::Eraser {
// Save the current tool override before switching
state.stylus_pre_eraser_tool_override = state.input_state.tool_override();
state.input_state.set_tool_override(Some(Tool::Eraser));
state.stylus_auto_switched_to_eraser = true;
info!(
"Auto-switched to eraser (physical eraser detected), saved previous: {:?}",
state.stylus_pre_eraser_tool_override
);
}
}

Expand Down
2 changes: 2 additions & 0 deletions src/backend/wayland/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ mod overlay_passthrough;
mod session;
mod state;
mod surface;
#[cfg(tablet)]
mod tablet_types;
mod toolbar;
mod toolbar_intent;
mod zoom;

pub use backend::WaylandBackend;
#[cfg(tablet)]
pub use tablet_types::TabletToolType;
Loading