forked from 1b-agent/opencadstudio-upstream
feat(cursor): add crosshair options
Add persistent cursor sizing, pick aperture, color, pointer mode, isometric drafting planes, and SNAPANG-aligned grid behavior.\n\nRefs #413
This commit is contained in:
1 parent
188ac80b2e
commit
1738f9a79b
25 files changed
+989
-126
No files matched your search
+69
-41
@@ -6,15 +6,23 @@ use iced::widget::canvas;
|
||||
use iced::{Color, Element, Length, Point, Size, Theme};
|
||||
|
||||
use crate::app::Message;
|
||||
use crate::app::settings::{CursorType, IsoPlane};
|
||||
use crate::scene::model::object::GripShape;
|
||||
use crate::scene::SelectionState;
|
||||
|
||||
/// Half-size of the crosshair center square in screen pixels (square = SQ*2 × SQ*2).
|
||||
pub const CROSSHAIR_SQ: f32 = 7.5;
|
||||
/// Arm length of the crosshair from center — used as the snap aperture radius.
|
||||
pub const CROSSHAIR_ARM: f32 = 60.0;
|
||||
use crate::snap::SnapType;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct CrosshairOptions {
|
||||
pub size_percent: i32,
|
||||
pub pick_box: i32,
|
||||
pub cursor_type: CursorType,
|
||||
pub color: Option<[u8; 3]>,
|
||||
pub isometric: bool,
|
||||
pub iso_plane: IsoPlane,
|
||||
pub snap_angle_deg: f32,
|
||||
}
|
||||
|
||||
// ── Grip marker data ──────────────────────────────────────────────────────
|
||||
|
||||
/// Describes one grip to be drawn in the viewport overlay.
|
||||
@@ -223,6 +231,7 @@ pub fn selection_overlay<'a>(
|
||||
suppressed: bool,
|
||||
hover_locked: bool,
|
||||
crosshair_bg: [f32; 4],
|
||||
crosshair: CrosshairOptions,
|
||||
) -> Element<'a, Message> {
|
||||
canvas(SelectionCanvas {
|
||||
selection,
|
||||
@@ -243,6 +252,7 @@ pub fn selection_overlay<'a>(
|
||||
suppressed,
|
||||
hover_locked,
|
||||
crosshair_bg,
|
||||
crosshair,
|
||||
})
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
@@ -297,6 +307,7 @@ struct SelectionCanvas {
|
||||
/// Background of the active drawing space. Crosshair contrast follows this
|
||||
/// rather than the UI theme, which may be light over a dark model viewport.
|
||||
crosshair_bg: [f32; 4],
|
||||
crosshair: CrosshairOptions,
|
||||
}
|
||||
|
||||
fn draw_grip_marker(frame: &mut canvas::Frame, grip: &GripMarker, theme: &Theme) {
|
||||
@@ -432,7 +443,7 @@ impl canvas::Program<Message> for SelectionCanvas {
|
||||
// cursor entirely. `Interaction::None` would let the stack fall
|
||||
// through to a sibling — `Hidden` is the explicit "no cursor"
|
||||
// signal that actually suppresses the OS arrow.
|
||||
if cursor.is_over(bounds) {
|
||||
if cursor.is_over(bounds) && self.crosshair.cursor_type == CursorType::Crosshair {
|
||||
mouse::Interaction::Hidden
|
||||
} else {
|
||||
mouse::Interaction::default()
|
||||
@@ -924,11 +935,28 @@ impl canvas::Program<Message> for SelectionCanvas {
|
||||
// top of it would double up the visual feedback.
|
||||
let over_divider = self.divider_under(cursor, bounds);
|
||||
// PAN mode replaces the crosshair with a hand cursor.
|
||||
if !over_viewcube && !over_divider && !self.pan_mode && !self.suppressed {
|
||||
if !over_viewcube
|
||||
&& !over_divider
|
||||
&& !self.pan_mode
|
||||
&& !self.suppressed
|
||||
&& self.crosshair.cursor_type == CursorType::Crosshair
|
||||
{
|
||||
if let Some(cp) = self.selection.last_move_pos {
|
||||
let [r, g, b, a] = crate::scene::view::render::adapt_to_bg(
|
||||
[1.0, 1.0, 1.0, 0.90],
|
||||
self.crosshair_bg,
|
||||
let [r, g, b, a] = self.crosshair.color.map_or_else(
|
||||
|| {
|
||||
crate::scene::view::render::adapt_to_bg(
|
||||
[1.0, 1.0, 1.0, 0.90],
|
||||
self.crosshair_bg,
|
||||
)
|
||||
},
|
||||
|[r, g, b]| {
|
||||
[
|
||||
r as f32 / 255.0,
|
||||
g as f32 / 255.0,
|
||||
b as f32 / 255.0,
|
||||
0.90,
|
||||
]
|
||||
},
|
||||
);
|
||||
let color = Color { r, g, b, a };
|
||||
let stroke = canvas::Stroke {
|
||||
@@ -936,38 +964,38 @@ impl canvas::Program<Message> for SelectionCanvas {
|
||||
style: canvas::Style::Solid(color),
|
||||
..Default::default()
|
||||
};
|
||||
let sq = CROSSHAIR_SQ; // square half-size → 15×15
|
||||
let arm = CROSSHAIR_ARM; // crosshair arm length from center
|
||||
|
||||
// Horizontal arms (start at square edge, end at arm length)
|
||||
let h_left = canvas::Path::new(|b| {
|
||||
b.move_to(Point::new(cp.x - sq, cp.y));
|
||||
b.line_to(Point::new(cp.x - arm, cp.y));
|
||||
});
|
||||
let h_right = canvas::Path::new(|b| {
|
||||
b.move_to(Point::new(cp.x + sq, cp.y));
|
||||
b.line_to(Point::new(cp.x + arm, cp.y));
|
||||
});
|
||||
// Vertical arms
|
||||
let v_top = canvas::Path::new(|b| {
|
||||
b.move_to(Point::new(cp.x, cp.y - sq));
|
||||
b.line_to(Point::new(cp.x, cp.y - arm));
|
||||
});
|
||||
let v_bot = canvas::Path::new(|b| {
|
||||
b.move_to(Point::new(cp.x, cp.y + sq));
|
||||
b.line_to(Point::new(cp.x, cp.y + arm));
|
||||
});
|
||||
// Center square
|
||||
let square = canvas::Path::rectangle(
|
||||
Point::new(cp.x - sq, cp.y - sq),
|
||||
Size::new(sq * 2.0, sq * 2.0),
|
||||
);
|
||||
|
||||
frame.stroke(&h_left, stroke.clone());
|
||||
frame.stroke(&h_right, stroke.clone());
|
||||
frame.stroke(&v_top, stroke.clone());
|
||||
frame.stroke(&v_bot, stroke.clone());
|
||||
frame.stroke(&square, stroke);
|
||||
let sq = self.crosshair.pick_box.clamp(0, 50) as f32;
|
||||
let arm = bounds.width.hypot(bounds.height)
|
||||
* self.crosshair.size_percent.clamp(1, 100) as f32
|
||||
/ 100.0;
|
||||
let base_angles: [f64; 2] = if self.crosshair.isometric {
|
||||
self.crosshair.iso_plane.angles()
|
||||
} else {
|
||||
[0.0, 90.0]
|
||||
};
|
||||
for angle in base_angles {
|
||||
let rad = (angle + self.crosshair.snap_angle_deg as f64).to_radians();
|
||||
let dir = Point::new(rad.cos() as f32, -rad.sin() as f32);
|
||||
let gap = if sq > 0.0 {
|
||||
sq / dir.x.abs().max(dir.y.abs()).max(1e-6)
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let arms = canvas::Path::new(|path| {
|
||||
path.move_to(Point::new(cp.x + dir.x * gap, cp.y + dir.y * gap));
|
||||
path.line_to(Point::new(cp.x + dir.x * arm, cp.y + dir.y * arm));
|
||||
path.move_to(Point::new(cp.x - dir.x * gap, cp.y - dir.y * gap));
|
||||
path.line_to(Point::new(cp.x - dir.x * arm, cp.y - dir.y * arm));
|
||||
});
|
||||
frame.stroke(&arms, stroke.clone());
|
||||
}
|
||||
if sq > 0.0 {
|
||||
let square = canvas::Path::rectangle(
|
||||
Point::new(cp.x - sq, cp.y - sq),
|
||||
Size::new(sq * 2.0, sq * 2.0),
|
||||
);
|
||||
frame.stroke(&square, stroke);
|
||||
}
|
||||
|
||||
// Locked-layer badge: a small padlock beside the crosshair when
|
||||
// the hovered object sits on a locked layer (issue: locked
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
//! OpenCADStudio-style OSNAP status menu.
|
||||
|
||||
use iced::widget::{button, container, row, text};
|
||||
use iced::widget::{button, checkbox, column, container, row, text};
|
||||
use iced::{Background, Element, Fill, Length, Theme};
|
||||
|
||||
use crate::app::Message;
|
||||
use crate::app::settings::IsoPlane;
|
||||
use crate::snap::{SnapType, Snapper, ALL_SNAP_MODES};
|
||||
use crate::ui::statusbar::status_menu::Entry;
|
||||
use crate::t;
|
||||
|
||||
pub fn menu_entries<'a>(snapper: &'a Snapper) -> Vec<Entry<'a>> {
|
||||
pub fn menu_entries<'a>(
|
||||
snapper: &'a Snapper,
|
||||
isometric: bool,
|
||||
iso_plane: IsoPlane,
|
||||
) -> Vec<Entry<'a>> {
|
||||
let all_on = snapper.all_on();
|
||||
let none_on = snapper.none_on();
|
||||
|
||||
@@ -30,7 +35,49 @@ pub fn menu_entries<'a>(snapper: &'a Snapper) -> Vec<Entry<'a>> {
|
||||
.width(Fill)
|
||||
.padding([0, 4]);
|
||||
|
||||
let mut entries = vec![Entry::stay(header), Entry::stay(divider)];
|
||||
let mut iso_planes = row![].spacing(3);
|
||||
for plane in IsoPlane::ALL {
|
||||
iso_planes = iso_planes.push(
|
||||
button(text(t!(plane.label())).size(10))
|
||||
.on_press(Message::SetIsoPlane(plane))
|
||||
.style(if isometric && iso_plane == plane {
|
||||
button::primary
|
||||
} else {
|
||||
button::secondary
|
||||
})
|
||||
.padding([3, 8]),
|
||||
);
|
||||
}
|
||||
let drafting = column![
|
||||
row![
|
||||
checkbox(isometric)
|
||||
.on_toggle(|_| Message::ToggleIsometricDrafting)
|
||||
.size(14),
|
||||
text(t!("Isometric drafting")).size(11),
|
||||
]
|
||||
.spacing(6)
|
||||
.align_y(iced::Center),
|
||||
iso_planes,
|
||||
text(t!("F5 cycles the active plane.")).size(10),
|
||||
]
|
||||
.spacing(5)
|
||||
.padding([5, 8]);
|
||||
let drafting_divider = container(iced::widget::Space::new().height(1))
|
||||
.style(|theme: &Theme| container::Style {
|
||||
background: Some(Background::Color(
|
||||
theme.palette().background.weak.color,
|
||||
)),
|
||||
..Default::default()
|
||||
})
|
||||
.width(Fill)
|
||||
.padding([0, 4]);
|
||||
|
||||
let mut entries = vec![
|
||||
Entry::stay(header),
|
||||
Entry::stay(divider),
|
||||
Entry::stay(drafting),
|
||||
Entry::stay(drafting_divider),
|
||||
];
|
||||
for &(snap_type, _glyph, label) in ALL_SNAP_MODES {
|
||||
entries.push(Entry::stay(snap_row(
|
||||
snap_type,
|
||||
|
||||
@@ -71,6 +71,8 @@ impl StatusBar {
|
||||
polar_increment_deg: f32,
|
||||
dyn_input: bool,
|
||||
otrack: bool,
|
||||
isometric_drafting: bool,
|
||||
iso_plane: crate::app::settings::IsoPlane,
|
||||
layouts: Vec<String>,
|
||||
block_tabs: Vec<String>,
|
||||
reorderable_layouts: Vec<String>,
|
||||
@@ -297,7 +299,11 @@ impl StatusBar {
|
||||
osnap_active,
|
||||
snapper.snap_enabled,
|
||||
tooltip_hidden,
|
||||
crate::ui::popup::snap_popup::menu_entries(snapper),
|
||||
crate::ui::popup::snap_popup::menu_entries(
|
||||
snapper,
|
||||
isometric_drafting,
|
||||
iso_plane,
|
||||
),
|
||||
)
|
||||
.into(),
|
||||
);
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
use crate::app::settings::IsoPlane;
|
||||
use crate::app::Message;
|
||||
use crate::snap::{Snapper, ALL_SNAP_MODES};
|
||||
use iced::widget::{button, checkbox, column, container, row, scrollable, text, Space};
|
||||
use iced::{Element, Fill};
|
||||
|
||||
pub fn view_window<'a>(
|
||||
snapper: &'a Snapper,
|
||||
grid: bool,
|
||||
grid_snap: bool,
|
||||
ortho: bool,
|
||||
polar: bool,
|
||||
otrack: bool,
|
||||
isometric: bool,
|
||||
iso_plane: IsoPlane,
|
||||
snap_angle_deg: f32,
|
||||
sizing: crate::ui::modal::ModalSizing,
|
||||
) -> Element<'a, Message> {
|
||||
let toggle = |value: bool, label: std::borrow::Cow<'a, str>, message: Message| {
|
||||
row![
|
||||
checkbox(value).on_toggle(move |_| message.clone()).size(15),
|
||||
text(label).size(12),
|
||||
]
|
||||
.spacing(7)
|
||||
.align_y(iced::Center)
|
||||
};
|
||||
|
||||
let drafting_modes = column![
|
||||
text(crate::t!("Drafting modes")).size(15),
|
||||
Space::new().height(8),
|
||||
toggle(grid, crate::t!("Grid display"), Message::ToggleGrid),
|
||||
toggle(grid_snap, crate::t!("Grid snap"), Message::ToggleGridSnap),
|
||||
toggle(ortho, crate::t!("Ortho"), Message::ToggleOrtho),
|
||||
toggle(polar, crate::t!("Polar tracking"), Message::TogglePolar),
|
||||
toggle(otrack, crate::t!("Object snap tracking"), Message::ToggleOTrack),
|
||||
]
|
||||
.spacing(6);
|
||||
|
||||
let mut planes = row![].spacing(5);
|
||||
for plane in IsoPlane::ALL {
|
||||
planes = planes.push(
|
||||
button(text(crate::t!(plane.label())).size(11))
|
||||
.on_press(Message::SetIsoPlane(plane))
|
||||
.style(if isometric && iso_plane == plane {
|
||||
button::primary
|
||||
} else {
|
||||
button::secondary
|
||||
})
|
||||
.padding([5, 12]),
|
||||
);
|
||||
}
|
||||
let isometric_controls = column![
|
||||
text(crate::t!("Isometric drafting")).size(15),
|
||||
Space::new().height(8),
|
||||
toggle(
|
||||
isometric,
|
||||
crate::t!("Enable isometric drafting"),
|
||||
Message::ToggleIsometricDrafting,
|
||||
),
|
||||
planes,
|
||||
text(crate::t!("F5 cycles Left, Top, and Right.")).size(11),
|
||||
Space::new().height(6),
|
||||
row![
|
||||
text(crate::t!("Rotation: %{angle}°", angle = snap_angle_deg)).size(11),
|
||||
button(text(crate::t!("Reset rotation")).size(10))
|
||||
.on_press(Message::ResetDraftingRotation)
|
||||
.style(button::secondary)
|
||||
.padding([4, 10]),
|
||||
]
|
||||
.spacing(10)
|
||||
.align_y(iced::Center),
|
||||
]
|
||||
.spacing(7);
|
||||
|
||||
let mut snap_modes = column![
|
||||
text(crate::t!("Object snap modes")).size(15),
|
||||
Space::new().height(8),
|
||||
toggle(
|
||||
snapper.snap_enabled,
|
||||
crate::t!("Enable object snap"),
|
||||
Message::ToggleSnapEnabled,
|
||||
),
|
||||
row![
|
||||
button(text(crate::t!("Select All")).size(10))
|
||||
.on_press(Message::SnapSelectAll)
|
||||
.style(button::secondary)
|
||||
.padding([4, 10]),
|
||||
button(text(crate::t!("Clear All")).size(10))
|
||||
.on_press(Message::SnapClearAll)
|
||||
.style(button::secondary)
|
||||
.padding([4, 10]),
|
||||
]
|
||||
.spacing(6),
|
||||
]
|
||||
.spacing(5);
|
||||
for &(snap_type, _, label) in ALL_SNAP_MODES {
|
||||
snap_modes = snap_modes.push(
|
||||
row![
|
||||
checkbox(snapper.is_on(snap_type))
|
||||
.on_toggle(move |_| Message::ToggleSnap(snap_type))
|
||||
.size(14),
|
||||
text(crate::t!(label)).size(11),
|
||||
]
|
||||
.spacing(7)
|
||||
.align_y(iced::Center),
|
||||
);
|
||||
}
|
||||
|
||||
let close = button(text(crate::tr!("action-close")).size(12))
|
||||
.on_press(Message::CloseModal)
|
||||
.padding([6, 18])
|
||||
.style(button::secondary);
|
||||
let content = column![
|
||||
drafting_modes,
|
||||
Space::new().height(20),
|
||||
isometric_controls,
|
||||
Space::new().height(20),
|
||||
snap_modes,
|
||||
]
|
||||
.width(sizing.width);
|
||||
let body = column![
|
||||
scrollable(content).spacing(8).height(sizing.height),
|
||||
Space::new().height(12),
|
||||
row![Space::new().width(Fill), close],
|
||||
]
|
||||
.width(sizing.width)
|
||||
.height(sizing.height);
|
||||
|
||||
container(body)
|
||||
.style(container::rounded_box)
|
||||
.padding([16, 18])
|
||||
.width(sizing.width)
|
||||
.height(sizing.height)
|
||||
.into()
|
||||
}
|
||||
@@ -3,6 +3,7 @@ pub mod block_palette;
|
||||
pub mod layout_manager;
|
||||
pub mod layer_state_manager;
|
||||
pub mod drawing_units;
|
||||
pub mod drafting_settings;
|
||||
pub mod layer_translator;
|
||||
pub mod plot;
|
||||
pub mod print_all;
|
||||
|
||||
+138
-4
@@ -1,11 +1,20 @@
|
||||
use crate::app::config::UiThemeConfig;
|
||||
use crate::app::settings::CursorType;
|
||||
use crate::app::Message;
|
||||
use iced::widget::{
|
||||
button, column, container, row, scrollable, text, text_input, Space,
|
||||
button, column, container, row, scrollable, slider, text, text_input, Space,
|
||||
};
|
||||
use iced::{Background, Border, Element, Theme};
|
||||
use iced::{Background, Border, Element, Fill, Theme};
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub enum OptionsTab {
|
||||
#[default]
|
||||
General,
|
||||
Display,
|
||||
Selection,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct Labelled<T> {
|
||||
value: T,
|
||||
@@ -24,6 +33,12 @@ pub fn view_window<'a>(
|
||||
ui_theme: &'a UiThemeConfig,
|
||||
theme_color_inputs: &'a [String; 6],
|
||||
language: crate::i18n::Language,
|
||||
active_tab: OptionsTab,
|
||||
cursor_size: i32,
|
||||
pick_box: i32,
|
||||
cursor_type: CursorType,
|
||||
crosshair_color: Option<[u8; 3]>,
|
||||
crosshair_color_input: &'a str,
|
||||
sizing: crate::ui::modal::ModalSizing,
|
||||
) -> Element<'a, Message> {
|
||||
let selected_format = crate::io::SAVE_FORMAT_OPTIONS
|
||||
@@ -62,6 +77,18 @@ pub fn view_window<'a>(
|
||||
.find(|choice| choice.value == language)
|
||||
.cloned();
|
||||
|
||||
let cursor_options = CursorType::ALL
|
||||
.into_iter()
|
||||
.map(|value: CursorType| Labelled {
|
||||
label: crate::t!(value.label()).into_owned(),
|
||||
value,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let selected_cursor = cursor_options
|
||||
.iter()
|
||||
.find(|choice| choice.value == cursor_type)
|
||||
.cloned();
|
||||
|
||||
let palette = ui_theme.palette.to_iced();
|
||||
let colors = [
|
||||
(crate::tr!("options-color-background"), palette.background),
|
||||
@@ -104,7 +131,7 @@ pub fn view_window<'a>(
|
||||
.padding([6, 18])
|
||||
.style(button::secondary);
|
||||
|
||||
let content = column![
|
||||
let general = column![
|
||||
text(crate::tr!("options-language-section")).size(15),
|
||||
Space::new().height(10),
|
||||
row![
|
||||
@@ -154,7 +181,29 @@ pub fn view_window<'a>(
|
||||
))
|
||||
.size(11)
|
||||
.width(sizing.width),
|
||||
Space::new().height(22),
|
||||
]
|
||||
.spacing(0)
|
||||
.width(sizing.width);
|
||||
|
||||
let crosshair_rgb = crosshair_color.unwrap_or([255, 255, 255]);
|
||||
let crosshair_swatch = container(Space::new())
|
||||
.width(28)
|
||||
.height(22)
|
||||
.style(move |theme: &Theme| container::Style {
|
||||
background: Some(Background::Color(iced::Color::from_rgb8(
|
||||
crosshair_rgb[0],
|
||||
crosshair_rgb[1],
|
||||
crosshair_rgb[2],
|
||||
))),
|
||||
border: Border {
|
||||
color: theme.palette().background.strong.color,
|
||||
width: 1.0,
|
||||
radius: 3.0.into(),
|
||||
},
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let display = column![
|
||||
text(crate::tr!("options-theme-section")).size(15),
|
||||
Space::new().height(10),
|
||||
row![
|
||||
@@ -175,11 +224,96 @@ pub fn view_window<'a>(
|
||||
.width(sizing.width),
|
||||
Space::new().height(12),
|
||||
color_controls,
|
||||
Space::new().height(24),
|
||||
text(crate::t!("Crosshair")).size(15),
|
||||
Space::new().height(10),
|
||||
row![
|
||||
text(crate::t!("Crosshair size")).size(12).width(140),
|
||||
slider(1..=100, cursor_size.clamp(1, 100), Message::CursorSizeChanged)
|
||||
.step(1)
|
||||
.width(Fill),
|
||||
text(format!("{}%", cursor_size.clamp(1, 100)))
|
||||
.size(11)
|
||||
.width(44),
|
||||
]
|
||||
.spacing(10)
|
||||
.align_y(iced::Center),
|
||||
Space::new().height(10),
|
||||
row![
|
||||
text(crate::t!("Cursor type")).size(12).width(140),
|
||||
iced::widget::pick_list(
|
||||
selected_cursor,
|
||||
cursor_options,
|
||||
|choice| choice.label.clone(),
|
||||
)
|
||||
.on_select(|choice| Message::CursorTypeChanged(choice.value))
|
||||
.width(Fill),
|
||||
]
|
||||
.spacing(10)
|
||||
.align_y(iced::Center),
|
||||
Space::new().height(10),
|
||||
row![
|
||||
text(crate::t!("Crosshair color")).size(12).width(140),
|
||||
crosshair_swatch,
|
||||
text_input(crate::t!("#RRGGBB or blank").as_ref(), crosshair_color_input)
|
||||
.on_input(Message::CrosshairColorChanged)
|
||||
.width(150),
|
||||
]
|
||||
.spacing(10)
|
||||
.align_y(iced::Center),
|
||||
Space::new().height(6),
|
||||
text(crate::t!("Leave the color blank to keep automatic viewport contrast."))
|
||||
.size(11)
|
||||
.width(sizing.width),
|
||||
]
|
||||
.spacing(0)
|
||||
.width(sizing.width);
|
||||
|
||||
let selection = column![
|
||||
text(crate::t!("Selection")).size(15),
|
||||
Space::new().height(10),
|
||||
row![
|
||||
text(crate::t!("Pick box size")).size(12).width(140),
|
||||
slider(0..=50, pick_box.clamp(0, 50), Message::PickBoxChanged)
|
||||
.step(1)
|
||||
.width(Fill),
|
||||
text(pick_box.clamp(0, 50).to_string()).size(11).width(44),
|
||||
]
|
||||
.spacing(10)
|
||||
.align_y(iced::Center),
|
||||
Space::new().height(8),
|
||||
text(crate::t!(
|
||||
"Controls both the visible selection box and the click aperture."
|
||||
))
|
||||
.size(11)
|
||||
.width(sizing.width),
|
||||
]
|
||||
.spacing(0)
|
||||
.width(sizing.width);
|
||||
|
||||
let content: Element<'a, Message> = match active_tab {
|
||||
OptionsTab::General => general.into(),
|
||||
OptionsTab::Display => display.into(),
|
||||
OptionsTab::Selection => selection.into(),
|
||||
};
|
||||
|
||||
let tab_button = |label, tab| {
|
||||
let selected = active_tab == tab;
|
||||
button(text(label).size(12))
|
||||
.on_press(Message::OptionsTabChanged(tab))
|
||||
.padding([6, 14])
|
||||
.style(if selected { button::primary } else { button::secondary })
|
||||
};
|
||||
let tabs = row![
|
||||
tab_button(crate::t!("General"), OptionsTab::General),
|
||||
tab_button(crate::t!("Display"), OptionsTab::Display),
|
||||
tab_button(crate::t!("Selection"), OptionsTab::Selection),
|
||||
]
|
||||
.spacing(6);
|
||||
|
||||
let body = column![
|
||||
tabs,
|
||||
Space::new().height(12),
|
||||
// Keep the scrollbar in its own lane instead of floating over the
|
||||
// controls at the trailing edge of the Options content.
|
||||
scrollable(content).spacing(8).height(sizing.height),
|
||||
|
||||
Reference in new issue
Block a user