shellm/src/widgets/styles.rs

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()
}