Files
OneCAD-Draft/src/ui/style/form.rs
T

276 lines
8.1 KiB
Rust

//! Form building blocks shared by the in-canvas dialogs.
//!
//! The Plot dialog grew its own `label : control` rows, field and button
//! styles, and section dividers; every dialog that followed copied them. They
//! live here once so a new dialog reads like the existing ones and a theme
//! tweak lands everywhere.
use crate::app::Message;
use crate::ui::style::common::muted_style;
use iced::widget::{button, container, pick_list, radio, row, text, text_input, Space};
use iced::{Background, Border, Element, Length, Theme};
use std::borrow::Cow;
/// Standard dialog footer action button (OK, Cancel, Apply, Close, Help, etc.),
/// matching the Drafting Settings / Options appearance: 12px text, [6, 18] padding,
/// themed primary or secondary style.
pub fn dialog_button<'a>(
label: impl text::IntoFragment<'a>,
on_press: Message,
primary: bool,
) -> button::Button<'a, Message> {
dialog_button_styled(
label,
on_press,
if primary {
button::primary
} else {
button::secondary
},
)
}
/// Standard dialog action button with an explicit button style and optional on_press action.
pub fn dialog_button_styled_opt<'a>(
label: impl text::IntoFragment<'a>,
on_press: Option<Message>,
style: fn(&Theme, button::Status) -> button::Style,
) -> button::Button<'a, Message> {
button(text(label).size(12))
.on_press_maybe(on_press)
.padding([6, 18])
.style(style)
}
/// Standard dialog action button with an explicit button style (primary, secondary, danger, warning, etc.).
pub fn dialog_button_styled<'a>(
label: impl text::IntoFragment<'a>,
on_press: Message,
style: fn(&Theme, button::Status) -> button::Style,
) -> button::Button<'a, Message> {
dialog_button_styled_opt(label, Some(on_press), style)
}
/// Unified radio button helper ensuring consistent 16px size for exact pixel-grid concentric alignment.
pub fn form_radio<'a, T>(
label: impl Into<String>,
value: T,
selected: Option<T>,
on_click: impl Fn(T) -> Message + 'a,
) -> radio::Radio<'a, Message>
where
T: Copy + Eq + 'a,
{
radio(label, value, selected, on_click)
.size(16)
.spacing(6)
.text_size(11)
}
/// Width of the label column in a `label : control` row.
pub const LABEL_WIDTH: f32 = 92.0;
/// Dialog button: accent (primary action) or neutral.
pub fn button_style(accent: bool) -> impl Fn(&Theme, button::Status) -> button::Style {
move |theme: &Theme, status| {
let palette = theme.palette();
let pair = match (accent, status) {
(true, button::Status::Hovered | button::Status::Pressed) => palette.primary.strong,
(false, button::Status::Hovered | button::Status::Pressed) => palette.background.strong,
(true, _) => palette.primary.base,
_ => palette.background.weak,
};
button::Style {
background: Some(Background::Color(pair.color)),
text_color: pair.text,
border: Border {
color: palette.background.neutral.color,
width: 1.0,
radius: 4.0.into(),
},
shadow: iced::Shadow::default(),
snap: false,
}
}
}
/// Text field: base background, neutral border that turns primary on focus.
pub fn field_style(theme: &Theme, status: text_input::Status) -> text_input::Style {
let palette = theme.palette();
let border = match status {
text_input::Status::Focused { .. } => palette.primary.base.color,
_ => palette.background.neutral.color,
};
text_input::Style {
background: Background::Color(palette.background.base.color),
border: Border {
color: border,
width: 1.0,
radius: 3.0.into(),
},
icon: palette.background.base.text,
placeholder: palette.background.base.text.scale_alpha(0.48),
value: palette.background.base.text,
selection: palette.primary.base.color.scale_alpha(0.5),
}
}
/// One-pixel horizontal rule.
pub fn hdivider<'a>(width: Length) -> Element<'a, Message> {
container(Space::new().width(width).height(1))
.width(width)
.height(1)
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(theme.palette().background.neutral.color)),
..Default::default()
})
.into()
}
/// One-pixel vertical rule.
pub fn vseparator<'a>(height: Length) -> Element<'a, Message> {
container(Space::new().width(1).height(height))
.width(1)
.height(height)
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(theme.palette().background.neutral.color)),
..Default::default()
})
.into()
}
/// Alias kept for call sites that historically used `vsep`.
/// Same implementation as [`vseparator`].
pub fn vsep<'a>(height: Length) -> Element<'a, Message> {
vseparator(height)
}
/// Muted section heading.
pub fn section_label<'a>(label: Cow<'static, str>) -> Element<'a, Message> {
text(label).size(11).style(muted_style).into()
}
fn row_label<'a>(label: Cow<'static, str>) -> Element<'a, Message> {
text(label)
.size(11)
.style(muted_style)
.width(LABEL_WIDTH)
.into()
}
/// A `label : dropdown` row.
pub fn labeled_pick_list<'a, T>(
label: Cow<'static, str>,
options: Vec<T>,
selected: Option<T>,
on_select: impl Fn(T) -> Message + 'a,
width: Length,
) -> Element<'a, Message>
where
T: ToString + Clone + PartialEq + 'a,
{
let list = pick_list(selected, options, |value| value.to_string())
.on_select(on_select)
.text_size(12)
.padding([3, 6])
.width(width);
row![row_label(label), list]
.spacing(8)
.align_y(iced::Center)
.into()
}
/// A `label : dropdown` row that renders read-only when `enabled` is false.
pub fn labeled_pick_list_enabled<'a, T>(
label: Cow<'static, str>,
options: Vec<T>,
selected: Option<T>,
on_select: impl Fn(T) -> Message + 'a,
width: Length,
enabled: bool,
) -> Element<'a, Message>
where
T: ToString + Clone + PartialEq + 'a,
{
if enabled {
return labeled_pick_list(label, options, selected, on_select, width);
}
row![
row_label(label),
crate::ui::read_only::field(
selected
.map(|choice| choice.to_string())
.unwrap_or_default()
.as_str(),
12.0,
width,
),
]
.spacing(8)
.align_y(iced::Center)
.into()
}
/// A `label : text field` row.
pub fn labeled_field<'a>(
label: Cow<'static, str>,
value: &'a str,
on_input: impl Fn(String) -> Message + 'a,
width: f32,
) -> Element<'a, Message> {
row![
row_label(label),
text_input("", value)
.on_input(on_input)
.style(field_style)
.size(12)
.width(width),
]
.spacing(8)
.align_y(iced::Center)
.into()
}
/// A `label : text field` row that renders read-only when `enabled` is false.
pub fn labeled_field_enabled<'a>(
label: Cow<'static, str>,
value: &'a str,
on_input: impl Fn(String) -> Message + 'a,
width: f32,
enabled: bool,
) -> Element<'a, Message> {
if enabled {
return labeled_field(label, value, on_input, width);
}
row![
row_label(label),
crate::ui::read_only::field(value, 12.0, Length::Fixed(width)),
]
.spacing(8)
.align_y(iced::Center)
.into()
}
/// A `label : text field` pair with its own label width, for rows that put
/// several small values side by side.
pub fn labeled_field_compact<'a>(
label: Cow<'static, str>,
value: &'a str,
on_input: impl Fn(String) -> Message + 'a,
label_width: f32,
field_width: f32,
) -> Element<'a, Message> {
row![
text(label).size(11).style(muted_style).width(label_width),
text_input("", value)
.on_input(on_input)
.style(field_style)
.size(12)
.padding([3, 6])
.width(field_width),
]
.spacing(6)
.align_y(iced::Center)
.into()
}