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