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: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ The v0.9.23+ prebuilt `wayscriber` packages require glibc 2.39 and GTK 4.12. See
- Blur tool with four styles: soften, pixelate, secure (flattens the region to one color), and black out
- Spotlight tool: dims everything except the regions you draw, with optional 1×–4× magnification
- Multiline text and sticky notes with smoothing; optional text halos take their contrast from the background the label sits on, so a label stays readable over a board, a filled shape, or a frozen screen (disable with `[drawing] text_halo_enabled = false`; a live transparent board has no pixels to sample and falls back to the text color)
- Selection: <kbd>Alt</kbd>-drag, <kbd>V</kbd> tool, properties panel
- Selection: <kbd>Alt</kbd>-drag, <kbd>V</kbd> tool, properties panel (<kbd>Ctrl+Alt+P</kbd> or right-click → **Properties**): swatches from your quick-color palette plus **+** for the full color picker, −/+ steppers, on/off switches, the arrow head's Start/End, drawn arrow styles, and a lock toggle; scroll over a row to step it, and hover the title for the shape's ID and creation time
- Duplicate (<kbd>Ctrl+D</kbd>), delete (<kbd>Delete</kbd>), undo/redo
- Color picker, screen eyedropper with a magnified pixel loupe, palettes, size via hotkeys or scroll
- Text font cycling with <kbd>Shift+T</kbd> over a configurable list (`[drawing] font_cycle`); with text selected it restyles that text
Expand Down Expand Up @@ -1176,7 +1176,7 @@ Drag modifier mappings are configurable via `[drawing]` (`drag_tool`, `shift_dra

### Quick colors

The quick color palette is configurable with ordered `[[drawing.quick_colors]]` entries. The first eight entries map to the <kbd>R</kbd>/<kbd>G</kbd>/<kbd>B</kbd>/<kbd>Y</kbd>/<kbd>O</kbd>/<kbd>P</kbd>/<kbd>W</kbd>/<kbd>K</kbd> shortcuts; if fewer are configured by hand, missing shortcut positions use the built-in defaults. The implicit default toolbar palette also preserves Cyan, Purple, and Gray as expanded toolbar colors while the radial menu keeps its original first-eight color ring. Explicit entries beyond the first eight have no shortcut action binding and opt those extra colors into dense palette UIs, capped to the first 24 colors.
The quick color palette is configurable with ordered `[[drawing.quick_colors]]` entries. The first eight entries map to the <kbd>R</kbd>/<kbd>G</kbd>/<kbd>B</kbd>/<kbd>Y</kbd>/<kbd>O</kbd>/<kbd>P</kbd>/<kbd>W</kbd>/<kbd>K</kbd> shortcuts; if fewer are configured by hand, missing shortcut positions use the built-in defaults. The implicit default toolbar palette also preserves Cyan, Purple, and Gray as expanded toolbar colors while the radial menu keeps its original first-eight color ring. Explicit entries beyond the first eight have no shortcut action binding and opt those extra colors into dense palette UIs, capped to the first 24 colors. The top toolbar's style pill shows up to 12 of them (fewer when the strip is short of width) and the selection properties panel up to 17; the color picker reaches any color.

You can also recolor the palette without opening an editor: **right-click any swatch** to open the color picker for that slot. The swatch updates live as you drag, OK saves that one entry's color back to `config.toml` (leaving your other settings and comments alone, with a timestamped `.bak`), and Cancel restores it. The slot keeps its label and shortcut, so <kbd>R</kbd> still selects the red slot after you point it at a different red. Recoloring the swatch you are drawing with moves the live color along with it. Left-click still just selects a swatch, and the leftmost chip still opens the picker for the active tool's own color.

Expand Down
29 changes: 29 additions & 0 deletions src/backend/wayland/handlers/pointer/axis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,17 @@ impl WaylandState {
) {
return;
}
if routed.surface == InputSurface::Canvas
&& !self.toolbar_chrome.pointer_over_toolbar()
&& try_handle_properties_panel_axis(
&mut self.input_state,
self.render.text_measurer(),
event.position,
scroll_direction,
)
{
return;
}
let over_toolbar =
routed.surface == InputSurface::Toolbar || self.toolbar_chrome.pointer_over_toolbar();
let over_top_toolbar =
Expand Down Expand Up @@ -383,6 +394,24 @@ fn try_handle_board_picker_page_panel_axis(
true
}

/// A wheel tick over the properties panel steps the row under it, and one
/// anywhere else on the panel is swallowed rather than resizing the tool
/// behind it. A surface covering the panel (the color picker it opened) keeps
/// the tick.
fn try_handle_properties_panel_axis(
input_state: &mut InputState,
measurer: &crate::draw::TextMeasurer,
position: (f64, f64),
scroll_direction: i32,
) -> bool {
if scroll_direction == 0 || input_state.modal_owns_wheel() {
return false;
}
let x = position.0.round() as i32;
let y = position.1.round() as i32;
input_state.properties_panel_wheel_with(measurer, x, y, scroll_direction)
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
44 changes: 24 additions & 20 deletions src/backend/wayland/state/render/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,11 @@ impl WaylandState {
// Inline bars stand in for the layer-shell toolbar surfaces, but every
// popup and modal below must paint above them.
self.render_inline_toolbar_chrome(ctx, capture_picker);
// Below the pickers: the color picker the panel opens has to cover it.
self.render_properties_panel(ctx, width, height, capture_picker);
self.render_help_and_pickers(ctx, width, height, capture_picker);
self.render_radial_menu_and_feedback(ctx, width, height, capture_picker);
self.render_properties_and_context(ctx, width, height, capture_picker);
self.render_context_menu(ctx, capture_picker);
self.render_inline_and_modal_ui(ctx, width, height, capture_picker);
}

Expand Down Expand Up @@ -358,41 +360,43 @@ impl WaylandState {
crate::ui::render_blocked_feedback(ctx, &self.input_state, width, height);
}

fn render_properties_and_context(
fn render_properties_panel(
&mut self,
ctx: &cairo::Context,
width: u32,
height: u32,
capture_picker: bool,
) {
// Board and page menus open on top of the board picker, so the picker
// must not hide this pass. It never shares the screen with the
// properties panel. Both surfaces are laid out in screen space from
// zoom-aware anchors, so they draw over a zoomed view too.
if capture_picker {
self.input_state.clear_context_menu_layout();
// Laid out in screen space from a zoom-aware anchor, so it draws over
// a zoomed view too.
if capture_picker || !self.input_state.is_properties_panel_open() {
self.input_state.clear_properties_panel_layout();
return;
}
if self.input_state.is_properties_panel_open() {
self.input_state
.update_properties_panel_layout_with_resources(
self.render.ui_text(),
self.render.text_measurer(),
ctx,
width,
height,
);
} else {
self.input_state.clear_properties_panel_layout();
}
self.input_state
.update_properties_panel_layout_with_resources(
self.render.ui_text(),
self.render.text_measurer(),
ctx,
width,
height,
);
crate::ui::render_properties_panel_with_engine(
self.render.ui_text(),
ctx,
&self.input_state,
width,
height,
);
}

fn render_context_menu(&mut self, ctx: &cairo::Context, capture_picker: bool) {
// Board and page menus open on top of the board picker, so the picker
// must not hide this pass.
if capture_picker {
self.input_state.clear_context_menu_layout();
return;
}
// An open menu was already laid out along with this frame's damage.
if !self.input_state.is_context_menu_open() {
self.input_state.clear_context_menu_layout();
Expand Down
7 changes: 5 additions & 2 deletions src/backend/wayland/toolbar/layout/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,17 @@ fn narrow_viewports_drop_presets_then_overflow_items() {
state.set_toolbar_use_icons(true);
let mut snapshot = snapshot_from_state(&state);

// Unconstrained: presets shown, the pill's eight swatches available,
// Unconstrained: presets shown, the pill's full swatch row available,
// nothing dropped into the overflow.
let full = crate::backend::wayland::toolbar::view::top::plan_top_strip(
&crate::ui_text::UiTextEngine::default(),
&snapshot,
);
assert!(!full.drop_presets);
assert_eq!(full.swatch_count, 8);
assert_eq!(
full.swatch_count,
crate::ui::toolbar::model::TopStripPlan::MAX_QUICK_COLORS
);
assert!(full.dropped_tools.is_empty() && full.dropped_utilities.is_empty());
let full_width = top_size(&crate::ui_text::UiTextEngine::default(), &snapshot).0;

Expand Down
4 changes: 2 additions & 2 deletions src/backend/wayland/toolbar/view/top.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ const TOP_COMPACT_ISLAND_PAD: f64 = crate::ui::theme::toolbar::COMPACT_ISLAND_PA
pub(crate) use model::TopStripPlan;

/// Degrade the strip until it fits the viewport: quick swatches shrink
/// 8→6→4→0 first, then droppable items move into the overflow menu.
/// 12→8→6→4→0 first, then droppable items move into the overflow menu.
pub fn plan_top_strip(engine: &UiTextEngine, snapshot: &ToolbarSnapshot) -> TopStripPlan {
let mut plan = TopStripPlan::unconstrained();
if snapshot.top_minimized || snapshot.top_micro_active() {
Expand All @@ -77,7 +77,7 @@ pub fn plan_top_strip(engine: &UiTextEngine, snapshot: &ToolbarSnapshot) -> TopS
if fits(&plan) {
return plan;
}
for count in [6, 4, 0] {
for count in [8, 6, 4, 0] {
plan.swatch_count = count;
if fits(&plan) {
return plan;
Expand Down
2 changes: 2 additions & 0 deletions src/backend/wayland/toolbar/view/top/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1171,12 +1171,14 @@ fn style_pill_geometry_holds_per_tool_and_select_hides_the_pill() {
label: "Color".to_string(),
value: "Red".to_string(),
kind: crate::input::SelectionPropertyKind::Color,
state: crate::input::SelectionPropertyKind::Color.unknown_value(),
disabled: false,
},
crate::input::SelectionPropertyEntry {
label: "Thickness".to_string(),
value: "3.0px".to_string(),
kind: crate::input::SelectionPropertyKind::Thickness,
state: crate::input::SelectionPropertyKind::Thickness.unknown_value(),
disabled: false,
},
];
Expand Down
2 changes: 1 addition & 1 deletion src/input/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ pub use state::{
HelpOverlayCursorHint, HelpOverlayReleaseOutcome, InputHudActiveSource, InputHudEntry,
InputHudEntryKind, InputHudSettings, InputState, OutputFocusAction, RegionInputSource,
RegionPurposeTag, RegionSelectUiState, SelectionHandle, SelectionPropertyEntry,
SelectionPropertyKind, TextInputMode, ZoomAction,
SelectionPropertyKind, SelectionPropertyValue, TextInputMode, ZoomAction,
};
#[allow(unused_imports)]
pub(crate) use state::{DrawingStyle, IdleHandle};
Expand Down
8 changes: 7 additions & 1 deletion src/input/state/core/color_picker_popup/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,14 +96,20 @@ pub enum ColorPickerTarget {
QuickColor(usize),
/// The paper color draft of the board picker's appearance sheet.
BoardPaper,
/// The selected shapes, opened from the properties panel. Nothing is
/// previewed live: OK recolors the selection in one undoable edit and
/// Cancel leaves it untouched.
Selection,
}

impl ColorPickerTarget {
/// The quick-color slot this target recolors, if any.
pub fn slot(self) -> Option<usize> {
match self {
ColorPickerTarget::QuickColor(index) => Some(index),
ColorPickerTarget::Tool | ColorPickerTarget::BoardPaper => None,
ColorPickerTarget::Tool
| ColorPickerTarget::BoardPaper
| ColorPickerTarget::Selection => None,
}
}

Expand Down
51 changes: 50 additions & 1 deletion src/input/state/core/color_picker_popup/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,25 @@ impl InputState {
true
}

/// Opens the popup on the selection's color, from the properties panel's
/// "More colors" button. The panel stays open underneath so the accepted
/// color shows up in it. Returns false when nothing selected has a color.
pub(crate) fn open_color_picker_popup_for_selection_with_measurer(
&mut self,
measurer: &TextMeasurer,
) -> bool {
if !self.selection_has_editable_color() {
return false;
}
let color = self
.selection_primary_color()
.unwrap_or_else(|| self.color_for_tool(self.active_tool()));
self.discard_open_color_picker_recolor();
self.clear_properties_panel_pointer_state();
self.open_color_picker_popup_for(measurer, ColorPickerTarget::Selection, color);
true
}

/// Whether the open popup edits the paper sheet's color draft.
pub fn color_picker_popup_edits_board_paper(&self) -> bool {
self.color_picker_popup.target() == Some(ColorPickerTarget::BoardPaper)
Expand Down Expand Up @@ -160,6 +179,12 @@ impl InputState {
ModalSurface::ColorPicker,
ModalSurface::BoardPicker,
);
} else if target == ColorPickerTarget::Selection {
// Opened from the properties panel, which shows the result.
self.close_modals_for_open_keeping(
ModalSurface::ColorPicker,
ModalSurface::PropertiesPanel,
);
} else {
self.close_modals_for_open(ModalSurface::ColorPicker);
}
Expand All @@ -186,6 +211,7 @@ impl InputState {
}
}
Some(ColorPickerTarget::BoardPaper) => Cow::Borrowed("Paper Color"),
Some(ColorPickerTarget::Selection) => Cow::Borrowed("Selection Color"),
Some(ColorPickerTarget::Tool) | None => Cow::Borrowed("Select Color"),
}
}
Expand Down Expand Up @@ -237,7 +263,13 @@ impl InputState {
// The sheet's draft is the target; its own Apply commits.
let _ = self.board_appearance_palette(opaque(color));
}
ColorPickerPopupState::Hidden => {}
// Shapes change on OK only, as one undo entry; a live preview
// would record one per drag step.
ColorPickerPopupState::Open {
target: ColorPickerTarget::Selection,
..
}
| ColorPickerPopupState::Hidden => {}
}
}

Expand Down Expand Up @@ -269,6 +301,10 @@ impl InputState {

/// Applies the current color and closes the popup.
pub fn apply_color_picker_popup(&mut self) {
self.apply_color_picker_popup_with_measurer(&TextMeasurer::default());
}

fn apply_color_picker_popup_with_measurer(&mut self, measurer: &TextMeasurer) {
self.cancel_pending_color_picker_paste();
let mut applied_color = None;
if let ColorPickerPopupState::Open {
Expand Down Expand Up @@ -303,6 +339,17 @@ impl InputState {
// has to put it back.
self.color_picker_popup_preview(color);
}
// Decided by the shapes, not by the color the popup opened on: that
// was only the first shape's, so a selection whose shapes disagree in
// hue or opacity still takes an accepted color that matches it.
if let Some((_, ColorPickerTarget::Selection, _, color)) = applied_color
&& self.selection_recolor_changes(color)
{
self.finish_active_arrow_bend();
if self.apply_selection_color_value_with(measurer, color) {
self.note_recent_color(color);
}
}
if let Some((tool, target, original_color, color)) = applied_color
&& original_color != color
{
Expand Down Expand Up @@ -334,6 +381,8 @@ impl InputState {
// The draft already holds the color; the sheet's Apply decides
// whether the board changes, so nothing is dirty yet.
ColorPickerTarget::BoardPaper => {}
// Applied above, whether or not the color moved.
ColorPickerTarget::Selection => {}
}
}
self.color_picker_popup.hide();
Expand Down
7 changes: 6 additions & 1 deletion src/input/state/core/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,12 @@ pub use menus::{
ContextSubmenu, MenuCommand, SubmenuSide,
};
pub use menus::{SUBMENU_AIM_GRACE, SUBMENU_HOVER_DELAY};
pub use properties::{SelectionPropertyEntry, SelectionPropertyKind};
pub(crate) use properties::metrics as properties_panel_metrics;
pub use properties::{
PanelRect, PropertiesPanelHit, PropertiesPanelLayout, PropertiesPanelLock,
PropertiesPanelSwatch, PropertiesRowControl, PropertiesRowGeometry, SelectionPropertyEntry,
SelectionPropertyKind, SelectionPropertyValue, ShapePropertiesPanel,
};
pub use radial_menu::{
COMPASS_SLICES as RADIAL_COMPASS_SLICES, CompassDir, RADIAL_PAINT_DELAY, RadialMenuLayout,
RadialMenuPanel, RadialMenuState, RadialParent, RadialRingSwatch, RadialSegmentId, RadialSlice,
Expand Down
4 changes: 3 additions & 1 deletion src/input/state/core/modal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,9 @@ impl ModalSurface {
///
/// The properties panel is deliberately out. It docks beside the canvas
/// rather than over it, and the canvas stays drawable underneath — so the
/// wheel still means what it means everywhere else.
/// wheel still means what it means everywhere else. A tick over the panel
/// itself steps the row under the pointer; the axis handler routes that
/// before it gets here.
fn owns_wheel(self) -> bool {
!matches!(self, ModalSurface::PropertiesPanel)
}
Expand Down
Loading
Loading