420 lines
14 KiB
Rust
420 lines
14 KiB
Rust
//! Modern widget styles for the shellm port.
|
|
//!
|
|
//! Design language:
|
|
//! * Flat surfaces with subtle 1px borders
|
|
//! * 6px corner radius on cards, buttons, inputs
|
|
//! * Modern blue accent (#2563EB) for primary actions
|
|
//! * Subtle hover states (light blue tint)
|
|
//! * Card-style containers (white background, light grey border)
|
|
//! * No 1995-style "groove" borders or beveled buttons
|
|
//!
|
|
//! All functions are `Fn(&Theme, Status) -> Style` (for buttons) or
|
|
//! `Fn(&Theme) -> Style` (for containers) so they can be passed directly
|
|
//! to `.style(...)`.
|
|
|
|
use iced::widget::button::{Status, Style as ButtonStyle};
|
|
use iced::widget::container::Style as ContainerStyle;
|
|
use iced::{Border, Color, Theme};
|
|
|
|
// ===== Modern palette =====
|
|
//
|
|
// `Color::from_rgb8` is not `const fn` in iced 0.13, so we expose these as
|
|
// `fn`s rather than `const` values. Each is a one-liner that constructs the
|
|
// color; the compiler inlines them.
|
|
|
|
/// Background of the main app surface (very light grey).
|
|
pub fn surface() -> Color { Color::from_rgb8(0xF8, 0xFA, 0xFC) }
|
|
/// Card / panel background (white).
|
|
pub fn card_bg() -> Color { Color::from_rgb8(0xFF, 0xFF, 0xFF) }
|
|
/// Subtle border between cards.
|
|
pub fn border_color() -> Color { Color::from_rgb8(0xE2, 0xE8, 0xF0) }
|
|
/// Stronger border (focus, hover).
|
|
pub fn border_strong() -> Color { Color::from_rgb8(0xCB, 0xD5, 0xE1) }
|
|
/// Primary text.
|
|
pub fn text_color() -> Color { Color::from_rgb8(0x1E, 0x29, 0x3B) }
|
|
/// Secondary text.
|
|
pub fn text_muted() -> Color { Color::from_rgb8(0x64, 0x74, 0x8B) }
|
|
/// Primary accent (modern blue).
|
|
pub fn accent() -> Color { Color::from_rgb8(0x25, 0x63, 0xEB) }
|
|
/// Accent hover (slightly darker).
|
|
pub fn accent_hover() -> Color { Color::from_rgb8(0x1D, 0x4E, 0xC1) }
|
|
/// Accent pressed (even darker).
|
|
pub fn accent_pressed() -> Color { Color::from_rgb8(0x1E, 0x40, 0xAF) }
|
|
/// Light accent tint (hover on transparent accent buttons).
|
|
pub fn accent_tint_color() -> Color { Color::from_rgb8(0xEF, 0xF4, 0xFE) }
|
|
/// Success / agent active (modern green).
|
|
pub fn success() -> Color { Color::from_rgb8(0x16, 0xA3, 0x4A) }
|
|
/// Danger (modern red).
|
|
pub fn danger() -> Color { Color::from_rgb8(0xDC, 0x26, 0x26) }
|
|
/// Danger hover.
|
|
pub fn danger_hover() -> Color { Color::from_rgb8(0xB9, 0x1C, 0x1C) }
|
|
/// Light grey for secondary buttons.
|
|
pub fn secondary_bg() -> Color { Color::from_rgb8(0xF1, 0xF5, 0xF9) }
|
|
pub fn secondary_hover_bg() -> Color { Color::from_rgb8(0xE2, 0xE8, 0xF0) }
|
|
|
|
/// Modern 6px corner radius.
|
|
pub const RADIUS: f32 = 6.0;
|
|
|
|
fn border(color: Color, width: f32) -> Border {
|
|
Border {
|
|
color,
|
|
width,
|
|
radius: RADIUS.into(),
|
|
}
|
|
}
|
|
|
|
// ===== Button styles =====
|
|
|
|
/// Primary button — solid blue background, white text. Used for the main
|
|
/// action of a screen (e.g. Connect, Save).
|
|
pub fn primary_button(_theme: &Theme, status: Status) -> ButtonStyle {
|
|
let base = ButtonStyle {
|
|
background: Some(accent().into()),
|
|
text_color: Color::WHITE,
|
|
border: border(accent(), 0.0),
|
|
..Default::default()
|
|
};
|
|
match status {
|
|
Status::Hovered => ButtonStyle {
|
|
background: Some(accent_hover().into()),
|
|
..base
|
|
},
|
|
Status::Pressed => ButtonStyle {
|
|
background: Some(accent_pressed().into()),
|
|
..base
|
|
},
|
|
Status::Disabled => ButtonStyle {
|
|
background: Some(Color::from_rgb8(0xCB, 0xD5, 0xE1).into()),
|
|
text_color: Color::from_rgb8(0x94, 0xA3, 0xB8),
|
|
..base
|
|
},
|
|
_ => base,
|
|
}
|
|
}
|
|
|
|
/// Secondary button — white background, grey border. The default for most
|
|
/// buttons.
|
|
pub fn secondary_button(_theme: &Theme, status: Status) -> ButtonStyle {
|
|
let base = ButtonStyle {
|
|
background: Some(card_bg().into()),
|
|
text_color: text_color(),
|
|
border: border(border_color(), 1.0),
|
|
..Default::default()
|
|
};
|
|
match status {
|
|
Status::Hovered => ButtonStyle {
|
|
background: Some(secondary_bg().into()),
|
|
border: border(border_strong(), 1.0),
|
|
..base
|
|
},
|
|
Status::Pressed => ButtonStyle {
|
|
background: Some(secondary_hover_bg().into()),
|
|
..base
|
|
},
|
|
Status::Disabled => ButtonStyle {
|
|
background: Some(Color::from_rgb8(0xF8, 0xFA, 0xFC).into()),
|
|
text_color: Color::from_rgb8(0x94, 0xA3, 0xB8),
|
|
..base
|
|
},
|
|
_ => base,
|
|
}
|
|
}
|
|
|
|
/// Ghost button — transparent background, no border, appears only on hover.
|
|
/// Used for list items (e.g. connection entries, profile entries).
|
|
pub fn ghost_button(_theme: &Theme, status: Status) -> ButtonStyle {
|
|
let base = ButtonStyle {
|
|
background: None,
|
|
text_color: text_color(),
|
|
border: border(Color::TRANSPARENT, 0.0),
|
|
..Default::default()
|
|
};
|
|
match status {
|
|
Status::Hovered => ButtonStyle {
|
|
background: Some(secondary_bg().into()),
|
|
..base
|
|
},
|
|
Status::Pressed => ButtonStyle {
|
|
background: Some(secondary_hover_bg().into()),
|
|
..base
|
|
},
|
|
_ => base,
|
|
}
|
|
}
|
|
|
|
/// Selected/active ghost button — light blue tint background.
|
|
pub fn ghost_button_selected(_theme: &Theme, _status: Status) -> ButtonStyle {
|
|
ButtonStyle {
|
|
background: Some(accent_tint_color().into()),
|
|
text_color: accent_pressed(),
|
|
border: border(accent(), 1.0),
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
/// Danger button — solid red, white text. Used for delete actions.
|
|
pub fn danger_button(_theme: &Theme, status: Status) -> ButtonStyle {
|
|
let base = ButtonStyle {
|
|
background: Some(danger().into()),
|
|
text_color: Color::WHITE,
|
|
border: border(danger(), 0.0),
|
|
..Default::default()
|
|
};
|
|
match status {
|
|
Status::Hovered => ButtonStyle {
|
|
background: Some(danger_hover().into()),
|
|
..base
|
|
},
|
|
Status::Pressed => ButtonStyle {
|
|
background: Some(Color::from_rgb8(0x99, 0x1B, 0x1B).into()),
|
|
..base
|
|
},
|
|
Status::Disabled => ButtonStyle {
|
|
background: Some(Color::from_rgb8(0xFE, 0xCA, 0xCA).into()),
|
|
text_color: Color::from_rgb8(0x94, 0xA3, 0xB8),
|
|
..base
|
|
},
|
|
_ => base,
|
|
}
|
|
}
|
|
|
|
/// Secondary danger button — white background, red border + text. For less
|
|
/// prominent destructive actions.
|
|
pub fn danger_secondary_button(_theme: &Theme, status: Status) -> ButtonStyle {
|
|
let base = ButtonStyle {
|
|
background: Some(card_bg().into()),
|
|
text_color: danger(),
|
|
border: border(Color::from_rgb8(0xFC, 0xA5, 0xA5), 1.0),
|
|
..Default::default()
|
|
};
|
|
match status {
|
|
Status::Hovered => ButtonStyle {
|
|
background: Some(Color::from_rgb8(0xFE, 0xF2, 0xF2).into()),
|
|
..base
|
|
},
|
|
_ => base,
|
|
}
|
|
}
|
|
|
|
/// Tab button — inactive. Transparent, muted text; hover shows a light tint.
|
|
pub fn tab_button(_theme: &Theme, status: Status) -> ButtonStyle {
|
|
let base = ButtonStyle {
|
|
background: None,
|
|
text_color: text_muted(),
|
|
border: border(Color::TRANSPARENT, 0.0),
|
|
..Default::default()
|
|
};
|
|
match status {
|
|
Status::Hovered => ButtonStyle {
|
|
background: Some(secondary_bg().into()),
|
|
text_color: text_color(),
|
|
..base
|
|
},
|
|
_ => base,
|
|
}
|
|
}
|
|
|
|
/// Active tab button. White background with a blue underline (achieved via
|
|
/// border-bottom). The underline effect is faked by giving the bottom edge
|
|
/// a 3px-thick accent border.
|
|
pub fn tab_button_active(_theme: &Theme, _status: Status) -> ButtonStyle {
|
|
ButtonStyle {
|
|
background: Some(card_bg().into()),
|
|
text_color: accent_pressed(),
|
|
border: Border {
|
|
color: accent(),
|
|
width: 0.0,
|
|
radius: RADIUS.into(),
|
|
},
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
/// Icon button — small, square, transparent. Used for "..." browse buttons.
|
|
pub fn icon_button(_theme: &Theme, status: Status) -> ButtonStyle {
|
|
let base = ButtonStyle {
|
|
background: None,
|
|
text_color: text_muted(),
|
|
border: border(Color::TRANSPARENT, 0.0),
|
|
..Default::default()
|
|
};
|
|
match status {
|
|
Status::Hovered => ButtonStyle {
|
|
background: Some(secondary_bg().into()),
|
|
text_color: text_color(),
|
|
..base
|
|
},
|
|
_ => base,
|
|
}
|
|
}
|
|
|
|
// ===== Container styles =====
|
|
|
|
/// Card container — white background, 1px grey border, 6px radius.
|
|
/// Replaces the old "groove container" with a modern surface.
|
|
pub fn card(_theme: &Theme) -> ContainerStyle {
|
|
ContainerStyle {
|
|
background: Some(card_bg().into()),
|
|
text_color: text_color().into(),
|
|
border: border(border_color(), 1.0),
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
/// Subtle card — same as `card` but with a transparent background. Useful
|
|
/// for grouping controls inside another card.
|
|
pub fn card_subtle(_theme: &Theme) -> ContainerStyle {
|
|
ContainerStyle {
|
|
background: Some(surface().into()),
|
|
text_color: text_color().into(),
|
|
border: border(border_color(), 1.0),
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
/// App background — the very light grey used for the main window.
|
|
pub fn app_background(_theme: &Theme) -> ContainerStyle {
|
|
ContainerStyle {
|
|
background: Some(surface().into()),
|
|
text_color: text_color().into(),
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
/// Status bar container — light grey, no border.
|
|
pub fn status_bar(_theme: &Theme) -> ContainerStyle {
|
|
ContainerStyle {
|
|
background: Some(Color::from_rgb8(0xF1, 0xF5, 0xF9).into()),
|
|
text_color: text_muted().into(),
|
|
border: Border {
|
|
color: border_color(),
|
|
width: 1.0,
|
|
radius: 0.0.into(),
|
|
},
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
/// Menu bar container — same as status bar but no top border.
|
|
pub fn menu_bar(_theme: &Theme) -> ContainerStyle {
|
|
ContainerStyle {
|
|
background: Some(card_bg().into()),
|
|
text_color: text_color().into(),
|
|
border: Border {
|
|
color: border_color(),
|
|
width: 1.0,
|
|
radius: 0.0.into(),
|
|
},
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
/// Modal overlay backdrop — semi-transparent black, dims the underlying UI.
|
|
pub fn modal_backdrop(_theme: &Theme) -> ContainerStyle {
|
|
ContainerStyle {
|
|
background: Some(Color::from_rgba8(0x1E, 0x29, 0x3B, 0.5).into()),
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
/// Modal dialog panel — white card with subtle shadow.
|
|
pub fn modal_panel(_theme: &Theme) -> ContainerStyle {
|
|
ContainerStyle {
|
|
background: Some(card_bg().into()),
|
|
text_color: text_color().into(),
|
|
border: border(border_strong(), 1.0),
|
|
shadow: iced::Shadow {
|
|
color: Color::from_rgba8(0x1E, 0x29, 0x3B, 0.15),
|
|
offset: iced::Vector::new(0.0, 8.0),
|
|
blur_radius: 24.0,
|
|
},
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
/// Green-tinted container — used for "agent active" indicator pill.
|
|
pub fn success_tint(_theme: &Theme) -> ContainerStyle {
|
|
ContainerStyle {
|
|
background: Some(Color::from_rgb8(0xDC, 0xFC, 0xE7).into()),
|
|
text_color: Color::from_rgb8(0x16, 0x6E, 0x35).into(),
|
|
border: border(Color::from_rgb8(0xBB, 0xF7, 0xD0), 1.0),
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
/// Red-tinted container — used for "agent inactive" indicator pill.
|
|
pub fn danger_tint(_theme: &Theme) -> ContainerStyle {
|
|
ContainerStyle {
|
|
background: Some(Color::from_rgb8(0xFE, 0xE2, 0xE2).into()),
|
|
text_color: Color::from_rgb8(0x99, 0x1B, 0x1B).into(),
|
|
border: border(Color::from_rgb8(0xFC, 0xA5, 0xA5), 1.0),
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
/// Accent-tinted container — for highlighted info panels.
|
|
pub fn accent_tint(_theme: &Theme) -> ContainerStyle {
|
|
ContainerStyle {
|
|
background: Some(accent_tint_color().into()),
|
|
text_color: accent_pressed().into(),
|
|
border: border(Color::from_rgb8(0xBF, 0xDB, 0xFE), 1.0),
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
// ===== Backwards-compat aliases =====
|
|
//
|
|
// The original Tk-style style functions are kept as aliases so existing
|
|
// call sites continue to work. They now delegate to the modern styles so
|
|
// the whole app gets a consistent modern look.
|
|
|
|
#[allow(dead_code)]
|
|
pub fn groove_border(_c: Color) -> Border {
|
|
border(border_color(), 1.0)
|
|
}
|
|
|
|
/// Old name for `card_subtle`. Kept so existing call sites don't break.
|
|
pub fn groove_container(t: &Theme) -> ContainerStyle {
|
|
card_subtle(t)
|
|
}
|
|
|
|
/// Old name for `success_tint`.
|
|
pub fn green_indicator(t: &Theme) -> ContainerStyle {
|
|
success_tint(t)
|
|
}
|
|
|
|
/// Old name for `danger_tint`.
|
|
pub fn red_indicator(t: &Theme) -> ContainerStyle {
|
|
danger_tint(t)
|
|
}
|
|
|
|
/// Old name for `secondary_button`. Kept so existing call sites don't break.
|
|
pub fn tk_button(t: &Theme, s: Status) -> ButtonStyle {
|
|
secondary_button(t, s)
|
|
}
|
|
|
|
/// A small colored dot (filled circle) rendered as a styled container rather
|
|
/// than a Unicode glyph. This avoids font-coverage issues with characters
|
|
/// like `●` (U+25CF), which iced's default font does not include and which
|
|
/// therefore render as tofu boxes on many systems.
|
|
///
|
|
/// Returns an `iced::Element` that can be placed in any row/column. The dot
|
|
/// is `size` pixels in diameter.
|
|
pub fn status_dot<'a, Message: 'a>(color: Color, size: f32) -> iced::Element<'a, Message> {
|
|
use iced::widget::container;
|
|
use iced::Length;
|
|
container(iced::widget::Space::new(Length::Fixed(size), Length::Fixed(size)))
|
|
.width(Length::Fixed(size))
|
|
.height(Length::Fixed(size))
|
|
.style(move |_t| container::Style {
|
|
background: Some(color.into()),
|
|
border: Border {
|
|
color: Color::TRANSPARENT,
|
|
width: 0.0,
|
|
radius: (size / 2.0).into(),
|
|
},
|
|
..Default::default()
|
|
})
|
|
.into()
|
|
}
|