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
18 changes: 18 additions & 0 deletions src/backend/wayland/toolbar/layout/side/arrow.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
use super::{SideLayoutContext, ToolbarLayoutSpec};
use crate::input::Tool;

// Arrow hit regions are built in render to avoid duplicate registrations.
pub(super) fn advance_arrow_section(ctx: &SideLayoutContext<'_>, y: f64) -> f64 {
let show_arrow_controls =
ctx.snapshot.active_tool == Tool::Arrow || ctx.snapshot.arrow_label_enabled;
if !show_arrow_controls {
return y;
}

let card_h = if ctx.snapshot.arrow_label_enabled {
ToolbarLayoutSpec::SIDE_TOGGLE_CARD_HEIGHT_WITH_RESET
} else {
ToolbarLayoutSpec::SIDE_TOGGLE_CARD_HEIGHT
};
y + card_h + ctx.section_gap
}
3 changes: 3 additions & 0 deletions src/backend/wayland/toolbar/layout/side/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
mod actions;
mod arrow;
mod colors;
mod delay;
mod drawer;
Expand Down Expand Up @@ -37,6 +38,8 @@ pub fn build_side_hits(
y += ToolbarLayoutSpec::SIDE_ERASER_MODE_CARD_HEIGHT + ctx.section_gap;
}

y = arrow::advance_arrow_section(&ctx, y);

let show_marker_opacity =
snapshot.show_marker_opacity_section || snapshot.thickness_targets_marker;
if show_marker_opacity {
Expand Down
3 changes: 3 additions & 0 deletions src/backend/wayland/toolbar/layout/spec/side/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ impl ToolbarLayoutSpec {
pub(in crate::backend::wayland::toolbar) const SIDE_STEP_SLIDER_TOP_PADDING: f64 = 4.0;
pub(in crate::backend::wayland::toolbar) const SIDE_SLIDER_CARD_HEIGHT: f64 = 52.0;
pub(in crate::backend::wayland::toolbar) const SIDE_ERASER_MODE_CARD_HEIGHT: f64 = 44.0;
pub(in crate::backend::wayland::toolbar) const SIDE_TOGGLE_CARD_HEIGHT: f64 = 44.0;
pub(in crate::backend::wayland::toolbar) const SIDE_TOGGLE_CARD_HEIGHT_WITH_RESET: f64 =
Self::SIDE_TOGGLE_CARD_HEIGHT + Self::SIDE_TOGGLE_HEIGHT + Self::SIDE_TOGGLE_GAP;
pub(in crate::backend::wayland::toolbar) const SIDE_FONT_CARD_HEIGHT: f64 = 50.0;
pub(in crate::backend::wayland::toolbar) const SIDE_DELAY_SECTION_HEIGHT: f64 = 55.0;
pub(in crate::backend::wayland::toolbar) const SIDE_TOGGLE_HEIGHT: f64 = 24.0;
Expand Down
11 changes: 11 additions & 0 deletions src/backend/wayland/toolbar/layout/spec/side/sizes.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::config::ToolbarLayoutMode;
use crate::input::Tool;
use crate::ui::toolbar::ToolbarSnapshot;

use super::super::ToolbarLayoutSpec;
Expand All @@ -14,6 +15,8 @@ impl ToolbarLayoutSpec {
snapshot.show_marker_opacity_section || snapshot.thickness_targets_marker;
let show_text_controls =
snapshot.text_active || snapshot.note_active || snapshot.show_text_controls;
let show_arrow_controls =
snapshot.active_tool == Tool::Arrow || snapshot.arrow_label_enabled;
let show_drawer_view =
snapshot.drawer_open && snapshot.drawer_tab == crate::input::ToolbarDrawerTab::View;
let show_advanced = snapshot.show_actions_advanced && show_drawer_view;
Expand Down Expand Up @@ -45,6 +48,14 @@ impl ToolbarLayoutSpec {
if snapshot.thickness_targets_eraser {
add_section(Self::SIDE_ERASER_MODE_CARD_HEIGHT, &mut height);
}
if show_arrow_controls {
let arrow_height = if snapshot.arrow_label_enabled {
Self::SIDE_TOGGLE_CARD_HEIGHT_WITH_RESET
} else {
Self::SIDE_TOGGLE_CARD_HEIGHT
};
add_section(arrow_height, &mut height);
}
if show_marker_opacity {
add_section(Self::SIDE_SLIDER_CARD_HEIGHT, &mut height);
}
Expand Down
92 changes: 92 additions & 0 deletions src/backend/wayland/toolbar/render/side_palette/arrow.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
use super::SidePaletteLayout;
use crate::backend::wayland::toolbar::events::HitKind;
use crate::backend::wayland::toolbar::hit::HitRegion;
use crate::backend::wayland::toolbar::layout::ToolbarLayoutSpec;
use crate::input::Tool;
use crate::ui::toolbar::ToolbarEvent;

use super::super::widgets::*;

pub(super) fn draw_arrow_section(layout: &mut SidePaletteLayout, y: &mut f64) {
let ctx = layout.ctx;
let snapshot = layout.snapshot;
let hits = &mut layout.hits;
let hover = layout.hover;
let x = layout.x;
let card_x = layout.card_x;
let card_w = layout.card_w;
let content_width = layout.content_width;
let section_gap = layout.section_gap;

let show_arrow_controls = snapshot.active_tool == Tool::Arrow || snapshot.arrow_label_enabled;
if !show_arrow_controls {
return;
}

let card_h = if snapshot.arrow_label_enabled {
ToolbarLayoutSpec::SIDE_TOGGLE_CARD_HEIGHT_WITH_RESET
} else {
ToolbarLayoutSpec::SIDE_TOGGLE_CARD_HEIGHT
};
draw_group_card(ctx, card_x, *y, card_w, card_h);
draw_section_label(
ctx,
x,
*y + ToolbarLayoutSpec::SIDE_SECTION_LABEL_OFFSET_TALL,
"Arrow labels",
);
if snapshot.arrow_label_enabled {
let hint = format!("Next: {}", snapshot.arrow_label_next);
let _ = ctx.save();
ctx.select_font_face("Sans", cairo::FontSlant::Normal, cairo::FontWeight::Normal);
ctx.set_font_size(10.0);
if let Ok(ext) = ctx.text_extents(&hint) {
let hint_x = card_x + card_w - ext.width() - 8.0 - ext.x_bearing();
let hint_y = *y + ToolbarLayoutSpec::SIDE_SECTION_LABEL_OFFSET_TALL;
ctx.set_source_rgba(0.7, 0.7, 0.75, 0.8);
ctx.move_to(hint_x, hint_y);
let _ = ctx.show_text(&hint);
}
let _ = ctx.restore();
}

let toggle_h = ToolbarLayoutSpec::SIDE_TOGGLE_HEIGHT;
let toggle_y = *y + ToolbarLayoutSpec::SIDE_SECTION_TOGGLE_OFFSET_Y;
let toggle_hover = hover
.map(|(hx, hy)| point_in_rect(hx, hy, x, toggle_y, content_width, toggle_h))
.unwrap_or(false);
draw_checkbox(
ctx,
x,
toggle_y,
content_width,
toggle_h,
snapshot.arrow_label_enabled,
toggle_hover,
"Auto-number",
);
hits.push(HitRegion {
rect: (x, toggle_y, content_width, toggle_h),
event: ToolbarEvent::ToggleArrowLabels(!snapshot.arrow_label_enabled),
kind: HitKind::Click,
tooltip: Some("Auto-number arrows 1, 2, 3.".to_string()),
});

if snapshot.arrow_label_enabled {
let reset_y =
toggle_y + ToolbarLayoutSpec::SIDE_TOGGLE_HEIGHT + ToolbarLayoutSpec::SIDE_TOGGLE_GAP;
let reset_hover = hover
.map(|(hx, hy)| point_in_rect(hx, hy, x, reset_y, content_width, toggle_h))
.unwrap_or(false);
draw_button(ctx, x, reset_y, content_width, toggle_h, false, reset_hover);
draw_label_center(ctx, x, reset_y, content_width, toggle_h, "Reset");
hits.push(HitRegion {
rect: (x, reset_y, content_width, toggle_h),
event: ToolbarEvent::ResetArrowLabelCounter,
kind: HitKind::Click,
tooltip: Some("Reset numbering to 1.".to_string()),
});
}

*y += card_h + section_gap;
}
2 changes: 2 additions & 0 deletions src/backend/wayland/toolbar/render/side_palette/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
mod actions;
mod arrow;
mod colors;
mod drawer;
mod header;
Expand Down Expand Up @@ -93,6 +94,7 @@ pub fn render_side_palette(
}

thickness::draw_thickness_section(&mut layout, &mut y);
arrow::draw_arrow_section(&mut layout, &mut y);
marker::draw_marker_opacity_section(&mut layout, &mut y);
text::draw_text_controls_section(&mut layout, &mut y);
drawer::draw_drawer_tabs(&mut layout, &mut y);
Expand Down
1 change: 1 addition & 0 deletions src/config/keybindings/actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ pub enum Action {
SelectHighlightTool,
IncreaseFontSize,
DecreaseFontSize,
ResetArrowLabelCounter,

// Board mode toggles
ToggleWhiteboard,
Expand Down
4 changes: 4 additions & 0 deletions src/config/keybindings/config/map/tools.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ impl KeybindingsConfig {
)?;
inserter.insert_all(&self.tools.increase_font_size, Action::IncreaseFontSize)?;
inserter.insert_all(&self.tools.decrease_font_size, Action::DecreaseFontSize)?;
inserter.insert_all(
&self.tools.reset_arrow_labels,
Action::ResetArrowLabelCounter,
)?;
Ok(())
}
}
4 changes: 4 additions & 0 deletions src/config/keybindings/config/types/bindings/tools.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ pub struct ToolKeybindingsConfig {

#[serde(default = "default_decrease_font_size")]
pub decrease_font_size: Vec<String>,

#[serde(default = "default_reset_arrow_labels")]
pub reset_arrow_labels: Vec<String>,
}

impl Default for ToolKeybindingsConfig {
Expand All @@ -73,6 +76,7 @@ impl Default for ToolKeybindingsConfig {
toggle_highlight_tool: default_toggle_highlight_tool(),
increase_font_size: default_increase_font_size(),
decrease_font_size: default_decrease_font_size(),
reset_arrow_labels: default_reset_arrow_labels(),
}
}
}
4 changes: 4 additions & 0 deletions src/config/keybindings/defaults/tools.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,7 @@ pub(crate) fn default_increase_font_size() -> Vec<String> {
pub(crate) fn default_decrease_font_size() -> Vec<String> {
vec!["Ctrl+Shift+-".to_string(), "Ctrl+Shift+_".to_string()]
}

pub(crate) fn default_reset_arrow_labels() -> Vec<String> {
vec!["Ctrl+Shift+R".to_string()]
}
6 changes: 6 additions & 0 deletions src/config/keybindings/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,12 @@ fn test_build_action_map() {
map.get(&toggle_highlight_tool),
Some(&Action::ToggleHighlightTool)
);

let reset_arrow_labels = KeyBinding::parse("Ctrl+Shift+R").unwrap();
assert_eq!(
map.get(&reset_arrow_labels),
Some(&Action::ResetArrowLabelCounter)
);
}

#[test]
Expand Down
2 changes: 1 addition & 1 deletion src/draw/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ pub use render::{
render_sticky_note, render_text,
};
#[allow(unused_imports)]
pub use shape::{EraserBrush, EraserKind, Shape, invalidate_text_cache};
pub use shape::{ArrowLabel, EraserBrush, EraserKind, Shape, invalidate_text_cache};

// Re-export color constants for public API (unused internally but part of public interface)
#[allow(unused_imports)]
Expand Down
32 changes: 32 additions & 0 deletions src/draw/render/shapes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use super::text::{render_sticky_note, render_text};
use super::types::EraserReplayContext;
use crate::draw::frame::DrawnShape;
use crate::draw::shape::Shape;
use crate::draw::shape::{ARROW_LABEL_BACKGROUND, arrow_label_layout};

/// Renders all shapes in a collection to a Cairo context.
///
Expand Down Expand Up @@ -92,7 +93,13 @@ pub fn render_shape(ctx: &cairo::Context, shape: &Shape) {
arrow_length,
arrow_angle,
head_at_end,
label,
} => {
let (tip_x, tip_y, tail_x, tail_y) = if *head_at_end {
(*x2, *y2, *x1, *y1)
} else {
(*x1, *y1, *x2, *y2)
};
render_arrow(
ctx,
*x1,
Expand All @@ -105,6 +112,31 @@ pub fn render_shape(ctx: &cairo::Context, shape: &Shape) {
*arrow_angle,
*head_at_end,
);
if let Some(label) = label {
let label_text = label.value.to_string();
if let Some(layout) = arrow_label_layout(
tip_x,
tip_y,
tail_x,
tail_y,
*thick,
&label_text,
label.size,
&label.font_descriptor,
) {
render_text(
ctx,
layout.x,
layout.y,
&label_text,
*color,
label.size,
&label.font_descriptor,
ARROW_LABEL_BACKGROUND,
None,
);
}
}
}
Shape::Text {
x,
Expand Down
74 changes: 74 additions & 0 deletions src/draw/shape/arrow_label.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
use crate::draw::FontDescriptor;
use crate::util::Rect;

use super::text::{text_bounds_from_metrics, text_layout_metrics};

pub(crate) const ARROW_LABEL_BACKGROUND: bool = true;

const LABEL_OFFSET_SCALE: f64 = 0.6;
const LABEL_OFFSET_MIN: f64 = 6.0;
const LABEL_THICKNESS_SCALE: f64 = 0.4;
const LABEL_ALONG_RATIO: f64 = 0.5;

pub(crate) struct ArrowLabelLayout {
pub(crate) x: i32,
pub(crate) y: i32,
pub(crate) bounds: Rect,
}

#[allow(clippy::too_many_arguments)]
pub(crate) fn arrow_label_layout(
tip_x: i32,
tip_y: i32,
tail_x: i32,
tail_y: i32,
thick: f64,
label_text: &str,
label_size: f64,
font_descriptor: &FontDescriptor,
) -> Option<ArrowLabelLayout> {
if label_text.is_empty() {
return None;
}

let dx = (tip_x - tail_x) as f64;
let dy = (tip_y - tail_y) as f64;
let len = (dx * dx + dy * dy).sqrt();
if len <= f64::EPSILON {
return None;
}

let ux = dx / len;
let uy = dy / len;
let nx = -uy;
let ny = ux;

let along = len * LABEL_ALONG_RATIO;
let offset =
(label_size * LABEL_OFFSET_SCALE).max(LABEL_OFFSET_MIN) + thick * LABEL_THICKNESS_SCALE;

let anchor_x = tail_x as f64 + ux * along + nx * offset;
let anchor_y = tail_y as f64 + uy * along + ny * offset;

let metrics = text_layout_metrics(label_text, label_size, font_descriptor, None)?;
let center_offset_x = metrics.ink_x + metrics.ink_width / 2.0;
let center_offset_y = metrics.ink_y + metrics.ink_height / 2.0;

let baseline_x = (anchor_x - center_offset_x).round();
let baseline_y = (anchor_y - center_offset_y + metrics.baseline).round();

let bounds = text_bounds_from_metrics(
baseline_x,
baseline_y,
&metrics,
label_size,
ARROW_LABEL_BACKGROUND,
None,
)?;

Some(ArrowLabelLayout {
x: baseline_x as i32,
y: baseline_y as i32,
bounds,
})
}
Loading