Files
wayscriber/src/ui/toolbar/model/style_pill.rs
T
devmobasa 2b21ff1d80 style: enable clippy::doc_markdown
Put identifiers and Wayland protocol names in doc comments in backticks
(`wl_output`, `zwp_tablet_v2`). Product names that docs write plainly
(GLib, HarfBuzz, HiDPI, JetBrains, MSBuild, StatusNotifierItem, VSync)
go in `doc-valid-idents` in clippy.toml, next to Clippy's default list.

Mechanical, with the lint enabled in Cargo.toml:

    cargo clippy --fix --workspace --all-targets --all-features
    cargo clippy --fix --workspace --all-targets --no-default-features
    cargo fmt --all

Two comments that the fix could not mark up cleanly were reworded by
hand: the surface module summary and the zoom-chip invariant.
2026-10-26 12:46:35 +01:00

436 lines
17 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// The shared toolbar model is being adopted incrementally by both frontends,
// and this file holds staged shapes no renderer reads yet. Scoped here rather
// than over the whole `model` module so the other ~11.5k lines - the specs,
// snapshot mapping, and settings panes that are fully live - are policed for
// dead code again.
#![allow(dead_code)]
//! Renderer-neutral specification of the contextual style pill (island D).
//!
//! The pill is a fourth detached island rendered under the top-strip
//! islands. It morphs with the active tool: the specification derives one
//! [`StylePillState`] from [`ToolContext`]/[`ToolOptionsKind`] and
//! enumerates the ordered control list for that state. Both frontends and
//! the contract tests consume this one structure, mirroring the
//! `TopToolbarSpec` pattern.
//!
//! Select (`ToolOptionsKind::None`) docks the current selection's
//! properties into the pill (`StylePillState::Selection`) while a
//! selection exists, and hides the pill otherwise. The docked controls
//! route through the same apply machinery as the overlay properties
//! popup, which stays available from the context menu.
#[cfg(test)]
use crate::config::QuickColorPalette;
use crate::config::{ToolbarStrokeControls, toolbar_item_ids as ids};
#[cfg(test)]
use crate::input::EraserMode;
use crate::input::{SelectionPropertyEntry, SelectionPropertyKind};
use crate::ui::toolbar::{ToolContext, ToolOptionsKind, ToolbarEvent, ToolbarSnapshot};
pub(crate) use arrow_menu::{
ARROW_STYLE_CHIP_GLYPH_H, ARROW_STYLE_CHIP_GLYPH_W, ARROW_STYLE_CHIP_W, ARROW_STYLE_MENU_INSET,
ARROW_STYLE_MENU_PAD, ARROW_STYLE_MENU_PREVIEW_H, ARROW_STYLE_MENU_PREVIEW_W,
ARROW_STYLE_MENU_ROW_GAP, ARROW_STYLE_MENU_ROW_H, ARROW_STYLE_MENU_ROW_W, ArrowStyleMenuEntry,
arrow_style_chip_label, arrow_style_menu_entries, arrow_style_menu_size,
};
pub(crate) use meter::{METER_ZERO_SLOT_W, StrokeSetting, StylePillMeter, StylePillMeterSegment};
pub(crate) use pen_feel::{
PEN_FEEL_BARS_H, PEN_FEEL_CONTENT_W, PEN_FEEL_HEADER_H, PEN_FEEL_HINT_H, PEN_FEEL_PAD,
PEN_FEEL_PREVIEW_H, PEN_FEEL_ROW_GAP, PEN_FEEL_SECTION_GAP, PEN_FEEL_TITLE_H, PenFeelSection,
pen_feel_id, pen_feel_panel_size, pen_feel_sections, pen_feel_settings, pen_feel_title,
};
use super::{ToolbarSliderSpec, TopStripPlan, toolbar_item_visible};
mod arrow_menu;
mod control;
mod meter;
mod pen_feel;
mod slider;
mod stepper;
pub(crate) use slider::{OpacityPaint, StylePillSlider};
/// Morph state of the style pill, derived from the active tool's options
/// kind. `Hidden` covers Select without a selection plus the
/// minimized/micro strip forms, where no contextual rows exist.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum StylePillState {
Hidden,
/// Select tool with an active selection: the pill docks the selection
/// properties (the same entry list as the overlay properties popup).
Selection,
Stroke,
Marker,
Eraser,
Shape,
Arrow,
StepMarker,
Spotlight,
Text,
}
impl StylePillState {
#[cfg(test)]
pub(crate) const fn key(self) -> &'static str {
match self {
Self::Hidden => "hidden",
Self::Selection => "selection",
Self::Stroke => "stroke",
Self::Marker => "marker",
Self::Eraser => "eraser",
Self::Shape => "shape",
Self::Arrow => "arrow",
Self::StepMarker => "step-marker",
Self::Spotlight => "spotlight",
Self::Text => "text",
}
}
}
/// Which auto-number counter a reset button targets.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum StylePillCounter {
Arrow,
Step,
}
/// One control in the pill's ordered list.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum StylePillControl {
/// Current-color chip; opens the big overlay gradient picker popup.
ColorChip,
/// Quick-color swatch by palette index (up to [`StylePillSpec::MAX_SWATCHES`]).
QuickSwatch(usize),
/// Thickness/size slider. The snapshot's `thickness` already targets
/// the eraser or marker size when those are active.
Slider(StylePillSlider),
/// Live thickness numeral; clicking opens the precise-entry popup.
ThicknessValue,
/// The Pen feel chip (`stroke_controls = "panel"`, the default): one
/// button standing in for pen smoothing and Shape Pen detection, opening
/// a panel with a meter for each, level names and hints, and a live
/// smoothing preview.
PenFeelChip,
/// Pen/marker smoothing, as a level meter with one bar per level
/// (`stroke_controls = "meter"`).
///
/// A meter rather than a slider: the range is seven whole passes, which
/// on a 110px track is 18px of travel per step and fiddly to land on. A
/// meter shows how high the level is and how far it goes, which a bare
/// "− 3 +" readout did not.
PenSmoothingMeter,
/// Shape Pen recognition sensitivity, as a level meter over its five
/// levels (`stroke_controls = "meter"`).
ShapeSensitivityMeter,
/// Pen/marker smoothing, as a −/value/+ stepper
/// (`stroke_controls = "stepper"`).
PenSmoothingStepper,
/// Shape Pen recognition sensitivity, as a −/value/+ stepper over its
/// five levels (`stroke_controls = "stepper"`).
ShapeSensitivityStepper,
/// Shape fill toggle.
FillToggle,
/// Arrow style chip, showing the style the next arrow will use drawn and
/// named. Clicking opens the arrow style menu, which lists every style as
/// a preview (see `arrow_menu.rs`); the keyboard action still cycles.
ArrowStyleChip,
/// Arrow auto-number toggle.
AutoNumberToggle,
/// Reset the arrow/step counter; tooltip carries the next number.
CounterReset(StylePillCounter),
/// Live text-size numeral; clicking opens the precise-entry popup.
FontSizeValue,
/// Bold on/off for selected text, or for the next label when no text is
/// selected. Font family and weight remain independent choices.
FontWeightToggle,
/// Button showing the family in use; opens the overlay's font picker over
/// every installed family.
FontFamilyPicker,
/// Brush/Stroke eraser mode segmented control (the old checkbox
/// semantics as a two-segment control emitting `SetEraserMode`).
EraserModeSegment,
/// Docked selection property rendered as a compact cycle button that
/// shows the entry's value; clicking steps the property forward
/// through the properties apply machinery (Color, Fill, `ArrowHead`,
/// `TextBackground`).
SelectionCycle(SelectionPropertyKind),
/// Docked numeric selection property rendered as a −/value/+ stepper;
/// the halves step the property through the properties apply
/// machinery (Thickness, `FontSize`, `ArrowLength`, `ArrowAngle`). The
/// machinery is relative (direction steps), so the pill uses steppers
/// where the tool states use sliders.
SelectionStepper(SelectionPropertyKind),
}
/// Presentation role of one pill control.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum StylePillRole {
Swatch,
Slider,
/// Live numeral button (opens the precise-entry popover).
Value,
Toggle,
Button,
Segmented,
/// −/value/+ stepper (docked numeric selection properties, and the tool
/// steppers of `stroke_controls = "stepper"`).
Stepper,
/// Row of level bars (pen smoothing, Shape Pen detection).
Meter,
}
/// One half of a pill segmented control.
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct StylePillSegment {
pub(crate) id: &'static str,
pub(crate) label: &'static str,
pub(crate) event: ToolbarEvent,
pub(crate) active: bool,
pub(crate) tooltip: String,
}
/// One half of a selection stepper (− or +).
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct StylePillStep {
pub(crate) id: &'static str,
pub(crate) label: &'static str,
pub(crate) event: ToolbarEvent,
pub(crate) tooltip: String,
}
/// Stable id fragment for one selection property kind.
pub(crate) const fn selection_kind_slug(kind: SelectionPropertyKind) -> &'static str {
match kind {
SelectionPropertyKind::Color => "color",
SelectionPropertyKind::Thickness => "thickness",
SelectionPropertyKind::Opacity => "opacity",
SelectionPropertyKind::Fill => "fill",
SelectionPropertyKind::FontSize => "font-size",
SelectionPropertyKind::ArrowHead => "arrow-head",
SelectionPropertyKind::ArrowStyle => "arrow-style",
SelectionPropertyKind::ArrowLength => "arrow-length",
SelectionPropertyKind::ArrowAngle => "arrow-angle",
SelectionPropertyKind::TextBackground => "text-background",
SelectionPropertyKind::SpotlightMagnification => "spotlight-magnification",
}
}
/// The pill control a docked selection entry maps to: relative numeric
/// properties become steppers, everything else a cycle button.
pub(crate) const fn selection_control_for_kind(kind: SelectionPropertyKind) -> StylePillControl {
match kind {
SelectionPropertyKind::Color
| SelectionPropertyKind::Fill
| SelectionPropertyKind::ArrowHead
| SelectionPropertyKind::ArrowStyle
| SelectionPropertyKind::TextBackground => StylePillControl::SelectionCycle(kind),
SelectionPropertyKind::Thickness
| SelectionPropertyKind::Opacity
| SelectionPropertyKind::FontSize
| SelectionPropertyKind::ArrowLength
| SelectionPropertyKind::ArrowAngle
| SelectionPropertyKind::SpotlightMagnification => StylePillControl::SelectionStepper(kind),
}
}
fn selection_entry(
snapshot: &ToolbarSnapshot,
kind: SelectionPropertyKind,
) -> Option<&SelectionPropertyEntry> {
snapshot
.selection_properties
.iter()
.find(|entry| entry.kind == kind)
}
/// The style pill for one snapshot: a morph state plus its ordered controls.
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct StylePillSpec {
state: StylePillState,
controls: Vec<StylePillControl>,
}
impl StylePillSpec {
/// Maximum quick-color swatches shown in the pill: always the strip's
/// quick-color cap, so widening one ladder can never silently leave
/// the other behind.
pub(crate) const MAX_SWATCHES: usize = TopStripPlan::MAX_QUICK_COLORS;
pub(crate) fn build(snapshot: &ToolbarSnapshot, plan: &TopStripPlan) -> Self {
let state = Self::state_of(snapshot, plan);
if state == StylePillState::Hidden {
return Self {
state,
controls: Vec::new(),
};
}
if state == StylePillState::Selection {
// Opacity stays in the properties panel: the strip is short of
// width, and a selection's opacity is not a mid-demo adjustment.
let mut controls: Vec<_> = snapshot
.selection_properties
.iter()
.filter(|entry| entry.kind != SelectionPropertyKind::Opacity)
.map(|entry| selection_control_for_kind(entry.kind))
.collect();
if snapshot.selection_has_text && !plan.drop_style_extras {
controls.push(StylePillControl::FontWeightToggle);
}
return Self { state, controls };
}
let context = ToolContext::from_snapshot(snapshot);
let mut controls = Vec::new();
if context.needs_color {
controls.push(StylePillControl::ColorChip);
// The quick-color swatch row is the on-strip home of the
// "Quick colors" customization toggle: M7 moved colors off the
// strip into this pill (M7-C1), so the toggle now gates these
// swatches (the color chip stays either way). Hiding the item
// hides the swatch row.
if toolbar_item_visible(snapshot, ids::TOP_GROUP_QUICK_COLORS) {
// Swatches follow the strip's width-degradation plan (12→8→6→4→0)
// so the pill narrows with the islands above it.
let count = snapshot
.quick_colors
.rendered_entries()
.len()
.min(Self::MAX_SWATCHES)
.min(plan.swatch_count);
controls.extend((0..count).map(StylePillControl::QuickSwatch));
}
}
if context.needs_thickness {
controls.push(StylePillControl::Slider(StylePillSlider::Thickness));
controls.push(StylePillControl::ThicknessValue);
}
if context.show_marker_opacity {
controls.push(StylePillControl::Slider(StylePillSlider::Opacity));
}
controls.extend(stroke_feel_controls(
snapshot.stroke_controls,
context.show_pen_smoothing && !plan.drop_style_extras,
context.show_shape_sensitivity && !plan.drop_style_extras,
));
if context.tool_options_kind == ToolOptionsKind::Spotlight {
controls.push(StylePillControl::Slider(
StylePillSlider::SpotlightMagnification,
));
}
if context.show_fill_toggle {
controls.push(StylePillControl::FillToggle);
}
if context.tool_options_kind == ToolOptionsKind::Arrow {
controls.push(StylePillControl::ArrowStyleChip);
}
if context.show_arrow_labels {
controls.push(StylePillControl::AutoNumberToggle);
if snapshot.arrow_label_enabled {
controls.push(StylePillControl::CounterReset(StylePillCounter::Arrow));
}
}
if context.show_step_counter {
controls.push(StylePillControl::CounterReset(StylePillCounter::Step));
}
if context.show_font_controls {
if context.show_font_size {
controls.push(StylePillControl::Slider(StylePillSlider::FontSize));
controls.push(StylePillControl::FontSizeValue);
}
if !plan.drop_style_extras {
controls.push(StylePillControl::FontWeightToggle);
}
controls.push(StylePillControl::FontFamilyPicker);
}
if context.show_eraser_mode {
controls.push(StylePillControl::EraserModeSegment);
}
Self { state, controls }
}
pub(crate) fn state(&self) -> StylePillState {
self.state
}
pub(crate) fn controls(&self) -> &[StylePillControl] {
&self.controls
}
/// Allocation-free visibility query for the sizing/planning paths. Every
/// non-hidden state materializes at least one control (pinned by test),
/// so this equals `!build(snapshot, plan).controls().is_empty()`.
pub(crate) fn visible(snapshot: &ToolbarSnapshot, plan: &TopStripPlan) -> bool {
Self::state_of(snapshot, plan) != StylePillState::Hidden
}
/// Allocation-free morph-state query matching [`Self::build`].
pub(crate) fn state_of(snapshot: &ToolbarSnapshot, plan: &TopStripPlan) -> StylePillState {
if snapshot.top_minimized || snapshot.top_micro_active() {
return StylePillState::Hidden;
}
// The last-resort compact presentation keeps only the protected
// strip core: the pill yields entirely under that width pressure.
if plan.compact {
return StylePillState::Hidden;
}
match ToolContext::from_snapshot(snapshot).tool_options_kind {
// Select: docks the selection properties while a selection
// exists; hidden otherwise.
ToolOptionsKind::None => {
if snapshot.selection_properties.is_empty() && !snapshot.selection_has_text {
StylePillState::Hidden
} else {
StylePillState::Selection
}
}
ToolOptionsKind::Stroke => StylePillState::Stroke,
ToolOptionsKind::Marker => StylePillState::Marker,
ToolOptionsKind::Eraser => StylePillState::Eraser,
ToolOptionsKind::Shape => StylePillState::Shape,
ToolOptionsKind::Arrow => StylePillState::Arrow,
ToolOptionsKind::StepMarker => StylePillState::StepMarker,
ToolOptionsKind::Spotlight => StylePillState::Spotlight,
ToolOptionsKind::Text => StylePillState::Text,
}
}
}
/// The pill's pen smoothing and Shape Pen detection controls in one
/// presentation style: the Pen feel chip standing in for whichever of the two
/// the tool uses, or an inline meter or stepper for each.
fn stroke_feel_controls(
style: ToolbarStrokeControls,
smoothing: bool,
detection: bool,
) -> Vec<StylePillControl> {
let (smoothing_control, detection_control) = match style {
ToolbarStrokeControls::Panel => {
return (smoothing || detection)
.then_some(StylePillControl::PenFeelChip)
.into_iter()
.collect();
}
ToolbarStrokeControls::Meter => (
StylePillControl::PenSmoothingMeter,
StylePillControl::ShapeSensitivityMeter,
),
ToolbarStrokeControls::Stepper => (
StylePillControl::PenSmoothingStepper,
StylePillControl::ShapeSensitivityStepper,
),
};
[
smoothing.then_some(smoothing_control),
detection.then_some(detection_control),
]
.into_iter()
.flatten()
.collect()
}
#[cfg(test)]
mod tests;