forked from 1b-agent/opencadstudio-upstream
276 lines
8.1 KiB
Rust
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()
|
|
}
|