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
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ https://github.com/user-attachments/assets/4b5ed159-8d1c-44cb-8fe4-e0f2ea41d818
### Boards
- Named boards with transparent overlay or custom backgrounds
- Isolated pages per board with auto-contrast pens
- Pan solid boards with <kbd>Space</kbd> + left-drag; reset from the context menu
- Jump slots: <kbd>Ctrl+Shift+1..9</kbd>
- Toggle whiteboard/blackboard
- Board picker: <kbd>Ctrl+Shift+B</kbd>
Expand Down Expand Up @@ -142,6 +143,7 @@ https://github.com/user-attachments/assets/4b5ed159-8d1c-44cb-8fe4-e0f2ea41d818
- Reset: <kbd>Ctrl+Alt+0</kbd>
- Lock view: <kbd>Ctrl+Alt+L</kbd>
- Pan: middle drag or arrow keys
- Right-click menu: <kbd>Zoom</kbd> → Zoom In / Zoom Out / Reset Zoom

---

Expand Down Expand Up @@ -524,6 +526,8 @@ Drag modifier mappings are configurable in `config.toml` via `[drawing]` (`drag_
| New board | <kbd>Ctrl+Shift+N</kbd> |
| Delete board | <kbd>Ctrl+Shift+Delete</kbd> |
| Board picker | <kbd>Ctrl+Shift+B</kbd> |
| Pan solid boards | Hold <kbd>Space</kbd> + left-drag |
| Reset solid-board pan | <kbd>Right-click</kbd> → Reset Canvas Position |

</details>

Expand Down
6 changes: 6 additions & 0 deletions config.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,12 @@ auto_create = true
# Show board name/slot in the status bar
show_board_badge = true

# Allow panning on solid-color boards with Space + left-drag
pan_enabled = true

# Show the pan hint in the status bar or as a floating badge
show_pan_badge = true

# Persist runtime edits (rename/background) back to config
persist_customizations = true

Expand Down
11 changes: 11 additions & 0 deletions docs/CONFIG.md
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,8 @@ Configure multiple boards (each with its own pages) plus the special transparent
max_count = 9
auto_create = true
show_board_badge = true
pan_enabled = true
show_pan_badge = true
persist_customizations = true
default_board = "transparent"

Expand Down Expand Up @@ -549,6 +551,8 @@ default_pen_color = { rgb = [0.969, 0.890, 0.784] }
- `max_count` — hard cap on total boards.
- `auto_create` — create a board when switching to an empty slot.
- `show_board_badge` — show board name/slot in the status bar.
- `pan_enabled` — allow panning on solid-color boards with <kbd>Space</kbd> + left-drag.
- `show_pan_badge` — show the pan hint in the status bar or as a floating badge.
- `persist_customizations` — runtime edits (rename/background) are written back to config.
- `default_board` — board id to activate on startup.
- `items` — ordered list of boards; each board has:
Expand All @@ -571,6 +575,13 @@ default_pen_color = { rgb = [0.969, 0.890, 0.784] }
- Modal list for switching, renaming, and recoloring boards.
- Inline edits persist immediately when `persist_customizations = true`.

**Solid-board pan:**
- Hold <kbd>Space</kbd> and drag with the left mouse button to pan whiteboards and other solid-color boards.
- Transparent overlay does not pan; it stays anchored to the live screen.
- The canvas context menu includes **Reset Canvas Position** when board panning is enabled.
- The same right-click menu exposes **Zoom** → **Zoom In**, **Zoom Out**, and **Reset Zoom**.
- Pan offsets are stored per page, so each page keeps its own position.

**CLI Override:**
Use a board id with `--mode`:
```bash
Expand Down
5 changes: 5 additions & 0 deletions src/backend/wayland/backend/state_init/input_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,9 @@ mod tests {
config.ui.show_floating_badge_always = true;
config.ui.active_output_badge = true;
config.ui.command_palette_toast_duration_ms = 1234;
let boards = config.boards.as_mut().expect("boards config");
boards.pan_enabled = false;
boards.show_pan_badge = false;

let input = build_input_state(&config);

Expand All @@ -189,6 +192,8 @@ mod tests {
assert!(input.show_floating_badge_always);
assert!(input.show_active_output_badge);
assert_eq!(input.command_palette_toast_duration_ms, 1234);
assert!(!input.boards.pan_enabled());
assert!(!input.boards.show_pan_badge());
}

#[test]
Expand Down
17 changes: 17 additions & 0 deletions src/backend/wayland/handlers/keyboard/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ impl KeyboardHandler for WaylandState {
// and breaking shortcuts/tools, aggressively reset our modifier state on
// focus loss.
self.input_state.reset_modifiers();
self.set_board_pan_key_held(false);
self.stop_board_pan();

if self.surface.is_xdg_window() && self.focus_exit_suppressed() {
warn!("Keyboard focus lost in xdg fallback; suppressing exit after clipboard action");
Expand Down Expand Up @@ -112,6 +114,11 @@ impl KeyboardHandler for WaylandState {
if self.try_handle_first_run_background_mode_choice(key) {
return;
}
if matches!(key, Key::Space) && self.should_capture_space_for_board_pan() {
self.set_board_pan_key_held(true);
self.input_state.needs_redraw = true;
return;
}
if self.zoom.is_engaged() {
match key {
Key::Escape => {
Expand Down Expand Up @@ -143,6 +150,7 @@ impl KeyboardHandler for WaylandState {
self.surface.width(),
self.surface.height(),
);
self.sync_input_zoom_state();
self.input_state.dirty_tracker.mark_full();
self.input_state.needs_redraw = true;
return;
Expand Down Expand Up @@ -180,6 +188,11 @@ impl KeyboardHandler for WaylandState {
) {
let key = keysym_to_key(event.keysym);
debug!("Key released: {:?}", key);
if matches!(key, Key::Space) && self.board_pan_key_held() {
self.set_board_pan_key_held(false);
self.input_state.needs_redraw = true;
return;
}
self.input_state.on_key_release(key);
}

Expand Down Expand Up @@ -216,6 +229,9 @@ impl KeyboardHandler for WaylandState {
return;
}
let key = keysym_to_key(event.keysym);
if matches!(key, Key::Space) && self.board_pan_key_held() {
return;
}
if self.zoom.active {
match key {
Key::Up | Key::Down | Key::Left | Key::Right => {
Expand All @@ -240,6 +256,7 @@ impl KeyboardHandler for WaylandState {
self.surface.width(),
self.surface.height(),
);
self.sync_input_zoom_state();
self.input_state.dirty_tracker.mark_full();
self.input_state.needs_redraw = true;
return;
Expand Down
18 changes: 14 additions & 4 deletions src/backend/wayland/handlers/pointer/cursor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,12 @@ impl WaylandState {
if self.toolbar_dragging() {
return CursorIcon::Grabbing;
}
if self.board_panning_active() {
return CursorIcon::Grabbing;
}
if self.board_pan_key_held() && self.can_start_board_pan() {
return CursorIcon::Grab;
}

// Inline toolbar cursor hints (when using inline mode)
if self.inline_toolbars_active()
Expand Down Expand Up @@ -183,8 +189,8 @@ impl WaylandState {
}

// Check if hovering over selection handles
let (mx, my) = self.current_mouse();
if let Some(handle) = self.input_state.hit_selection_handle(mx, my) {
let (canvas_x, canvas_y) = self.input_state.canvas_pointer_position();
if let Some(handle) = self.input_state.hit_selection_handle(canvas_x, canvas_y) {
return match handle {
SelectionHandle::TopLeft | SelectionHandle::BottomRight => CursorIcon::NwseResize,
SelectionHandle::TopRight | SelectionHandle::BottomLeft => CursorIcon::NeswResize,
Expand All @@ -194,12 +200,16 @@ impl WaylandState {
}

// Check if hovering over text resize handle
if self.input_state.hit_text_resize_handle(mx, my).is_some() {
if self
.input_state
.hit_text_resize_handle(canvas_x, canvas_y)
.is_some()
{
return CursorIcon::SeResize;
}

// Check if hovering over a selected shape (for move)
if let Some(hit_id) = self.input_state.hit_test_at(mx, my)
if let Some(hit_id) = self.input_state.hit_test_at(canvas_x, canvas_y)
&& self
.input_state
.selected_shape_ids_set()
Expand Down
10 changes: 8 additions & 2 deletions src/backend/wayland/handlers/pointer/enter_leave.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ impl WaylandState {
{
self.set_current_mouse(sx as i32, sy as i32);
let (wx, wy) = self.zoomed_world_coords(sx, sy);
self.input_state.update_pointer_position(wx, wy);
self.input_state
.update_pointer_positions(sx as i32, sy as i32, wx, wy);
} else {
self.set_current_mouse(event.position.0 as i32, event.position.1 as i32);
}
Expand All @@ -39,7 +40,12 @@ impl WaylandState {
if !on_toolbar {
self.set_current_mouse(event.position.0 as i32, event.position.1 as i32);
let (wx, wy) = self.zoomed_world_coords(event.position.0, event.position.1);
self.input_state.update_pointer_position(wx, wy);
self.input_state.update_pointer_positions(
event.position.0.round() as i32,
event.position.1.round() as i32,
wx,
wy,
);
if self.input_state.eraser_mode == EraserMode::Stroke
&& self.input_state.active_tool() == Tool::Eraser
{
Expand Down
45 changes: 41 additions & 4 deletions src/backend/wayland/handlers/pointer/motion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ impl WaylandState {
{
self.set_current_mouse(sx as i32, sy as i32);
let (wx, wy) = self.zoomed_world_coords(sx, sy);
self.input_state.update_pointer_position(wx, wy);
self.input_state
.update_pointer_positions(sx as i32, sy as i32, wx, wy);
}
let evt = self.toolbar.pointer_motion(&event.surface, event.position);
if self.toolbar_dragging() {
Expand All @@ -77,7 +78,12 @@ impl WaylandState {
if self.pointer_over_toolbar() {
self.set_current_mouse(event.position.0 as i32, event.position.1 as i32);
let (wx, wy) = self.zoomed_world_coords(event.position.0, event.position.1);
self.input_state.update_pointer_position(wx, wy);
self.input_state.update_pointer_positions(
event.position.0.round() as i32,
event.position.1.round() as i32,
wx,
wy,
);
let evt = self.toolbar.pointer_motion(&event.surface, event.position);
if self.toolbar_dragging() {
// Use move_drag_intent if pointer_motion didn't return an intent
Expand Down Expand Up @@ -118,17 +124,48 @@ impl WaylandState {
.update_pan_position(event.position.0, event.position.1);
self.zoom
.pan_by_screen_delta(dx, dy, self.surface.width(), self.surface.height());
self.sync_input_zoom_state();
let (wx, wy) = self.zoomed_world_coords(event.position.0, event.position.1);
self.input_state.update_pointer_positions(
event.position.0.round() as i32,
event.position.1.round() as i32,
wx,
wy,
);
self.input_state.dirty_tracker.mark_full();
self.input_state.needs_redraw = true;
return;
}
if self.board_panning_active() {
self.set_current_mouse(event.position.0 as i32, event.position.1 as i32);
let (dx, dy) = self.update_board_pan_position(event.position.0, event.position.1);
let _ = self.pan_board_by_screen_delta(dx, dy);
let (wx, wy) = self.zoomed_world_coords(event.position.0, event.position.1);
self.input_state.update_pointer_positions(
event.position.0.round() as i32,
event.position.1.round() as i32,
wx,
wy,
);
return;
}
self.set_current_mouse(event.position.0 as i32, event.position.1 as i32);
// Block pointer motion when modal overlays are active
if self.input_state.command_palette_open || 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);
self.input_state.update_pointer_positions(
event.position.0.round() as i32,
event.position.1.round() as i32,
wx,
wy,
);
self.input_state.on_mouse_motion_with_canvas(
event.position.0.round() as i32,
event.position.1.round() as i32,
wx,
wy,
);
}
}
10 changes: 9 additions & 1 deletion src/backend/wayland/handlers/pointer/press.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,11 @@ impl WaylandState {
self.input_state.needs_redraw = true;
return;
}
if button == BTN_LEFT && self.board_pan_key_held() && self.can_start_board_pan() {
self.start_board_pan(event.position.0, event.position.1);
self.input_state.needs_redraw = true;
return;
}

let mb = match button {
BTN_LEFT => MouseButton::Left,
Expand All @@ -117,8 +122,11 @@ impl WaylandState {
_ => return,
};

let screen_x = event.position.0.round() as i32;
let screen_y = event.position.1.round() as i32;
let (wx, wy) = self.zoomed_world_coords(event.position.0, event.position.1);
self.input_state.on_mouse_press(mb, wx, wy);
self.input_state
.on_mouse_press_with_canvas(mb, screen_x, screen_y, wx, wy);
self.input_state.needs_redraw = true;
}
}
10 changes: 9 additions & 1 deletion src/backend/wayland/handlers/pointer/release.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,11 @@ impl WaylandState {
}
return;
}
if button == BTN_LEFT && self.board_panning_active() {
self.stop_board_pan();
self.input_state.needs_redraw = true;
return;
}

let mb = match button {
BTN_LEFT => MouseButton::Left,
Expand All @@ -117,8 +122,11 @@ impl WaylandState {
}
}

let screen_x = event.position.0.round() as i32;
let screen_y = event.position.1.round() as i32;
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
.on_mouse_release_with_canvas(mb, screen_x, screen_y, wx, wy);
self.input_state.needs_redraw = true;
}
}
35 changes: 24 additions & 11 deletions src/backend/wayland/handlers/tablet/tool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,11 +144,16 @@ impl Dispatch<ZwpTabletToolV2, ()> for WaylandState {
state.current_mouse().0,
state.current_mouse().1
);
let (wx, wy) = state.zoomed_world_coords(
state.current_mouse().0 as f64,
state.current_mouse().1 as f64,
let screen_x = state.current_mouse().0;
let screen_y = state.current_mouse().1;
let (wx, wy) = state.zoomed_world_coords(screen_x as f64, screen_y as f64);
state.input_state.on_mouse_press_with_canvas(
MouseButton::Left,
screen_x,
screen_y,
wx,
wy,
);
state.input_state.on_mouse_press(MouseButton::Left, wx, wy);
state.input_state.needs_redraw = true;
}
Event::Up => {
Expand Down Expand Up @@ -186,13 +191,16 @@ impl Dispatch<ZwpTabletToolV2, ()> for WaylandState {
state.current_mouse().0,
state.current_mouse().1
);
let (wx, wy) = state.zoomed_world_coords(
state.current_mouse().0 as f64,
state.current_mouse().1 as f64,
let screen_x = state.current_mouse().0;
let screen_y = state.current_mouse().1;
let (wx, wy) = state.zoomed_world_coords(screen_x as f64, screen_y as f64);
state.input_state.on_mouse_release_with_canvas(
MouseButton::Left,
screen_x,
screen_y,
wx,
wy,
);
state
.input_state
.on_mouse_release(MouseButton::Left, wx, wy);
state.input_state.needs_redraw = true;
}
Event::Motion { x, y } => {
Expand Down Expand Up @@ -255,7 +263,12 @@ impl Dispatch<ZwpTabletToolV2, ()> for WaylandState {
state.current_mouse().0 as f64,
state.current_mouse().1 as f64,
);
state.input_state.on_mouse_motion(wx, wy);
state.input_state.on_mouse_motion_with_canvas(
x.round() as i32,
y.round() as i32,
wx,
wy,
);
if state.stylus_tip_down {
state.stylus_pressure_thickness = Some(state.input_state.current_thickness);
state.record_stylus_peak(state.input_state.current_thickness);
Expand Down
Loading
Loading