Files
RuDS/crates/bds-ui/src/components/inputs.rs

187 lines
5.6 KiB
Rust

use iced::widget::text::Shaping;
use iced::widget::{Space, button, checkbox, column, container, pick_list, row, text, text_input};
use iced::{Alignment, Background, Color, Element, Length, Theme};
/// Standard form field label color.
pub const LABEL_COLOR: Color = Color::from_rgb(0.65, 0.68, 0.75);
const _FIELD_BG: Color = Color::from_rgb(0.15, 0.16, 0.20);
pub const SECTION_COLOR: Color = Color::from_rgb(0.50, 0.52, 0.58);
const DANGER_BG: Color = Color::from_rgb(0.60, 0.15, 0.15);
const DANGER_HOVER: Color = Color::from_rgb(0.70, 0.20, 0.20);
const PRIMARY_BG: Color = Color::from_rgb(0.20, 0.40, 0.70);
const PRIMARY_HOVER: Color = Color::from_rgb(0.25, 0.48, 0.80);
/// A labeled text input field.
pub fn labeled_input<'a, Message: Clone + 'a>(
label: &str,
placeholder: &str,
value: &str,
on_change: impl Fn(String) -> Message + 'a,
) -> Element<'a, Message> {
column![
text(label.to_string())
.size(12)
.color(LABEL_COLOR)
.shaping(Shaping::Advanced),
text_input(placeholder, value).on_input(on_change).size(14),
]
.spacing(4)
.into()
}
/// A labeled select/dropdown field.
pub fn labeled_select<'a, T, Message>(
label: &str,
options: &[T],
selected: Option<&T>,
on_select: impl Fn(T) -> Message + 'a,
) -> Element<'a, Message>
where
T: ToString + PartialEq + Clone + 'a,
Message: Clone + 'a,
{
let list: Vec<T> = options.to_vec();
column![
text(label.to_string())
.size(12)
.color(LABEL_COLOR)
.shaping(Shaping::Advanced),
pick_list(list, selected.cloned(), on_select),
]
.spacing(4)
.into()
}
/// A labeled checkbox.
pub fn labeled_checkbox<'a, Message: Clone + 'a>(
label: &str,
is_checked: bool,
on_toggle: impl Fn(bool) -> Message + 'a,
) -> Element<'a, Message> {
checkbox(label, is_checked)
.on_toggle(on_toggle)
.size(16)
.text_size(14)
.into()
}
/// A section header with optional separator line.
pub fn section_header<'a, Message: 'a>(label: &str) -> Element<'a, Message> {
column![
text(label.to_string())
.size(11)
.color(SECTION_COLOR)
.shaping(Shaping::Advanced),
container(Space::new(0, 0))
.width(Length::Fill)
.height(Length::Fixed(1.0))
.style(|_: &Theme| container::Style {
background: Some(Background::Color(Color::from_rgb(0.25, 0.25, 0.30))),
..container::Style::default()
}),
]
.spacing(4)
.into()
}
/// Primary action button style.
pub fn primary_button(theme: &Theme, status: button::Status) -> button::Style {
let _ = theme;
match status {
button::Status::Hovered => button::Style {
background: Some(Background::Color(PRIMARY_HOVER)),
text_color: Color::WHITE,
border: iced::Border {
radius: 4.0.into(),
..iced::Border::default()
},
..button::Style::default()
},
_ => button::Style {
background: Some(Background::Color(PRIMARY_BG)),
text_color: Color::WHITE,
border: iced::Border {
radius: 4.0.into(),
..iced::Border::default()
},
..button::Style::default()
},
}
}
/// Danger action button style (delete, destructive).
pub fn danger_button(theme: &Theme, status: button::Status) -> button::Style {
let _ = theme;
match status {
button::Status::Hovered => button::Style {
background: Some(Background::Color(DANGER_HOVER)),
text_color: Color::WHITE,
border: iced::Border {
radius: 4.0.into(),
..iced::Border::default()
},
..button::Style::default()
},
_ => button::Style {
background: Some(Background::Color(DANGER_BG)),
text_color: Color::WHITE,
border: iced::Border {
radius: 4.0.into(),
..iced::Border::default()
},
..button::Style::default()
},
}
}
/// Toolbar-style row with right-aligned actions.
pub fn toolbar<'a, Message: 'a>(
left: Vec<Element<'a, Message>>,
right: Vec<Element<'a, Message>>,
) -> Element<'a, Message> {
let left_row = container(
iced::widget::Row::with_children(left)
.spacing(8)
.align_y(Alignment::Center),
)
.clip(true)
.width(Length::Fill);
let right_row = iced::widget::Row::with_children(right)
.spacing(8)
.align_y(Alignment::Center)
.wrap();
row![left_row, right_row]
.padding(8)
.spacing(8)
.align_y(Alignment::Center)
.width(Length::Fill)
.into()
}
/// Date display (read-only, locale-formatted).
pub fn date_label<'a, Message: 'a>(label: &str, timestamp_ms: i64) -> Element<'a, Message> {
let date_str = format_timestamp(timestamp_ms);
row![
text(label.to_string())
.size(12)
.color(LABEL_COLOR)
.shaping(Shaping::Advanced),
text(date_str)
.size(12)
.color(Color::from_rgb(0.55, 0.58, 0.65))
.shaping(Shaping::Advanced),
]
.spacing(8)
.into()
}
/// Format a Unix timestamp (ms) to a readable date string.
fn format_timestamp(ms: i64) -> String {
let secs = ms / 1000;
let (y, m, d) = bds_core::util::timestamp::year_month_day_from_unix_ms(ms);
let h = ((secs % 86400) / 3600) as u32;
let min = ((secs % 3600) / 60) as u32;
format!("{y}-{m:02}-{d:02} {h:02}:{min:02}")
}