From c1beeff6b23f92f775e19145746366ecd3022363 Mon Sep 17 00:00:00 2001 From: Suyog Tandel Date: Fri, 30 Jan 2026 21:21:26 +0530 Subject: [PATCH] chore(ui): abstract away cards into a card component --- src/ui/components/card.rs | 108 ++++++++++++ src/ui/components/mod.rs | 1 + src/ui/views/about.rs | 183 ++++++++++----------- src/ui/views/config.rs | 95 +++-------- src/ui/views/home.rs | 337 +++++++++++++++++--------------------- src/ui/views/logs.rs | 2 +- src/ui/views/passkeys.rs | 102 +++--------- 7 files changed, 394 insertions(+), 434 deletions(-) create mode 100644 src/ui/components/card.rs diff --git a/src/ui/components/card.rs b/src/ui/components/card.rs new file mode 100644 index 0000000..5f20fa9 --- /dev/null +++ b/src/ui/components/card.rs @@ -0,0 +1,108 @@ +use gpui::*; +use gpui_component::{ActiveTheme, Icon, Theme, h_flex, v_flex}; + +#[derive(IntoElement)] +pub struct Card { + title: Option, + description: Option, + icon: Option, + header_right: Option, + children: Vec, +} + +impl Card { + pub fn new() -> Self { + Self { + title: None, + description: None, + icon: None, + header_right: None, + children: Vec::new(), + } + } + + pub fn title(mut self, title: impl Into) -> Self { + self.title = Some(title.into()); + self + } + + pub fn description(mut self, description: impl Into) -> Self { + self.description = Some(description.into()); + self + } + + pub fn icon(mut self, icon: Icon) -> Self { + self.icon = Some(icon); + self + } + + pub fn header_right(mut self, element: impl IntoElement) -> Self { + self.header_right = Some(element.into_any_element()); + self + } +} + +impl ParentElement for Card { + fn extend(&mut self, elements: impl IntoIterator) { + self.children.extend(elements); + } +} + +impl RenderOnce for Card { + fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { + let theme: &Theme = cx.theme(); + + let has_header = self.title.is_some() + || self.icon.is_some() + || self.description.is_some() + || self.header_right.is_some(); + + let header = if has_header { + let mut left_side = v_flex().gap_1(); + + let mut icon_title_row = h_flex().items_center().gap_2(); + if let Some(icon) = self.icon { + icon_title_row = + icon_title_row.child(Icon::new(icon).size_5().text_color(theme.foreground)); + } + if let Some(title) = self.title { + icon_title_row = icon_title_row.child( + div() + .font_weight(FontWeight::BOLD) + .text_color(theme.foreground) + .child(title), + ); + } + + left_side = left_side.child(icon_title_row); + + if let Some(desc) = self.description { + left_side = left_side.child( + div() + .text_sm() + .text_color(theme.muted_foreground) + .child(desc), + ); + } + + let mut header_row = h_flex().items_center().justify_between().child(left_side); + + if let Some(right) = self.header_right { + header_row = header_row.child(right); + } + + Some(header_row.into_any_element()) + } else { + None + }; + + div() + .w_full() + .bg(rgb(0x18181b)) + .border_1() + .border_color(theme.border) + .rounded_xl() + .p_6() + .child(v_flex().gap_6().children(header).children(self.children)) + } +} diff --git a/src/ui/components/mod.rs b/src/ui/components/mod.rs index 28f3cd2..4f4ac29 100644 --- a/src/ui/components/mod.rs +++ b/src/ui/components/mod.rs @@ -1,3 +1,4 @@ pub mod button; +pub mod card; pub mod page_view; pub mod sidebar; diff --git a/src/ui/views/about.rs b/src/ui/views/about.rs index e1c4631..3b71302 100644 --- a/src/ui/views/about.rs +++ b/src/ui/views/about.rs @@ -1,5 +1,5 @@ // src/views/about.rs -use crate::ui::components::page_view::PageView; +use crate::ui::components::{card::Card, page_view::PageView}; use gpui::*; use gpui_component::{Icon, StyledExt, Theme, badge::Badge, button::Button, h_flex, v_flex}; @@ -16,100 +16,97 @@ impl AboutView { .justify_center() .child( div() - .max_w(px(1200.0)) .w_full() - .bg(rgb(0x18181b)) - .border_1() - .border_color(theme.border) - .rounded_xl() - .p_6() + .max_w(px(1200.0)) .child( - v_flex() - .items_center() - .justify_center() - .gap_4() - .py_8() - .text_center() - .child( - img("appIcons/in.suyogtandel.picoforge.svg") - .w(px(256.0)) - .h(px(256.0)), - ) - .child( - div() - .text_2xl() - .font_bold() - .text_color(theme.foreground) - .child("PicoForge"), - ) - .child(Badge::new().child("v0.4.0").color(theme.secondary)) - .child( - div() - .text_color(theme.muted_foreground) - .max_w(px(450.0)) - .child( - "An open source commissioning tool for Pico FIDO security keys. Developed with Rust and GPUI.", - ), - ) - .child( - div() - .text_sm() - .text_color(theme.muted_foreground) - .gap_1() - .pt_4() - .border_t_1() - .border_color(theme.border) - .border_t_1() - .border_color(theme.border) - .w(px(320.0)) - .child( - h_flex() - .justify_between() - .child("Code By:") - .child( - div() - .font_medium() - .text_color(theme.foreground) - .child("Suyog Tandel, Fabrice Bellamy"), - ), - ) - .child( - h_flex() - .justify_between() - .items_center() - .pt_2() - .mt_2() - .child(h_flex().items_center().gap_1().child("Copyright:")) - .child( - div() - .font_medium() - .text_color(theme.foreground) - .child("© 2026 Suyog Tandel"), - ), - ), - ) - .child( - h_flex() - .gap_4() - .pt_6() - .child( - Button::new("github_btn") - .outline() - .child( - h_flex() - .gap_2() - .child( - Icon::default() - .path("icons/github.svg") - .size_4(), - ) - .child("GitHub"), - ) - .on_click(|_, _, cx| { - cx.open_url("https://github.com/librekeys/picoforge") - }), - ), - ), + Card::new().child( + v_flex() + .items_center() + .justify_center() + .gap_4() + .py_8() + .text_center() + .child( + img("appIcons/in.suyogtandel.picoforge.svg") + .w(px(256.0)) + .h(px(256.0)), + ) + .child( + div() + .text_2xl() + .font_bold() + .text_color(theme.foreground) + .child("PicoForge"), + ) + .child(Badge::new().child("v0.4.0").color(theme.secondary)) + .child( + div() + .text_color(theme.muted_foreground) + .max_w(px(450.0)) + .child( + "An open source commissioning tool for Pico FIDO security keys. Developed with Rust and GPUI.", + ), + ) + .child( + div() + .text_sm() + .text_color(theme.muted_foreground) + .gap_1() + .pt_4() + .border_t_1() + .border_color(theme.border) + .border_t_1() + .border_color(theme.border) + .w(px(320.0)) + .child( + h_flex() + .justify_between() + .child("Code By:") + .child( + div() + .font_medium() + .text_color(theme.foreground) + .child("Suyog Tandel, Fabrice Bellamy"), + ), + ) + .child( + h_flex() + .justify_between() + .items_center() + .pt_2() + .mt_2() + .child(h_flex().items_center().gap_1().child("Copyright:")) + .child( + div() + .font_medium() + .text_color(theme.foreground) + .child("© 2026 Suyog Tandel"), + ), + ), + ) + .child( + h_flex() + .gap_4() + .pt_6() + .child( + Button::new("github_btn") + .outline() + .child( + h_flex() + .gap_2() + .child( + Icon::default() + .path("icons/github.svg") + .size_4(), + ) + .child("GitHub"), + ) + .on_click(|_, _, cx| { + cx.open_url("https://github.com/librekeys/picoforge") + }), + ), + ), + ), ), ), theme, diff --git a/src/ui/views/config.rs b/src/ui/views/config.rs index 8c330d6..4b1b573 100644 --- a/src/ui/views/config.rs +++ b/src/ui/views/config.rs @@ -1,10 +1,10 @@ use crate::device::io; use crate::device::types::{AppConfigInput, FullDeviceStatus}; -use crate::ui::components::page_view::PageView; +use crate::ui::components::{card::Card, page_view::PageView}; use crate::ui::ui_types::VENDORS; use gpui::*; use gpui_component::{ - ActiveTheme, Disableable, Icon, StyledExt, Theme, + ActiveTheme, Disableable, Icon, Theme, button::Button, input::{Input, InputState}, select::{Select, SelectItem, SelectState}, @@ -432,13 +432,11 @@ impl ConfigView { .child(Input::new(&self.product_name_input)), ); - Self::config_card( - "Identity", - "USB Identification settings", - Icon::default().path("icons/tag.svg"), - content, - theme, - ) + Card::new() + .title("Identity") + .description("USB Identification settings") + .icon(Icon::default().path("icons/tag.svg")) + .child(content) } fn render_led_card(&mut self, cx: &mut Context) -> impl IntoElement { @@ -522,13 +520,11 @@ impl ConfigView { ), ); - Self::config_card( - "LED Settings", - "Adjust visual feedback behavior", - Icon::default().path("icons/microchip.svg"), - content, - theme, - ) + Card::new() + .title("LED Settings") + .description("Adjust visual feedback behavior") + .icon(Icon::default().path("icons/microchip.svg")) + .child(content) } fn render_touch_card(&self, theme: &Theme) -> impl IntoElement { @@ -539,13 +535,11 @@ impl ConfigView { .child(Input::new(&self.touch_timeout_input)), ); - Self::config_card( - "Touch & Timing", - "Configure interaction timeouts", - Icon::default().path("icons/settings.svg"), - content, - theme, - ) + Card::new() + .title("Touch & Timing") + .description("Configure interaction timeouts") + .icon(Icon::default().path("icons/settings.svg")) + .child(content) } fn render_options_card(&mut self, cx: &mut Context) -> impl IntoElement { @@ -600,56 +594,11 @@ impl ConfigView { ), ); - Self::config_card( - "Device Options", - "Toggle advanced features", - Icon::default().path("icons/settings.svg"), - content, - &theme, - ) - } - - fn config_card( - title: &str, - description: &str, - icon: Icon, - content: impl IntoElement, - theme: &Theme, - ) -> impl IntoElement { - div() - .w_full() - .bg(rgb(0x18181b)) // Using the same bg as home card - .border_1() - .border_color(theme.border) - .rounded_xl() - .p_6() - .child( - v_flex() - .gap_6() - .child( - v_flex() - .gap_1() - .child( - gpui_component::h_flex() - .items_center() - .gap_2() - .child(Icon::new(icon).size_5().text_color(theme.foreground)) - .child( - div() - .font_bold() - .text_color(theme.foreground) - .child(title.to_string()), - ), - ) - .child( - div() - .text_sm() - .text_color(theme.muted_foreground) - .child(description.to_string()), - ), - ) - .child(content), - ) + Card::new() + .title("Device Options") + .description("Toggle advanced features") + .icon(Icon::default().path("icons/settings.svg")) + .child(content) } } diff --git a/src/ui/views/home.rs b/src/ui/views/home.rs index 70f0b0e..3fc0cd5 100644 --- a/src/ui/views/home.rs +++ b/src/ui/views/home.rs @@ -1,5 +1,5 @@ use crate::device::types::DeviceMethod; -use crate::ui::components::page_view::PageView; +use crate::ui::components::{card::Card, page_view::PageView}; use crate::ui::ui_types::GlobalDeviceState; use gpui::*; use gpui_component::StyledExt; @@ -46,39 +46,6 @@ impl HomeView { ) } - fn home_card( - title: &str, - icon: Icon, - content: impl IntoElement, - theme: &Theme, - ) -> impl IntoElement { - div() - .w_full() - // TODO: REPLACE with a constant or modify default theme - .bg(rgb(0x18181b)) - .border_1() - .border_color(theme.border) - .rounded_xl() - .p_6() - .child( - v_flex() - .gap_6() - .child( - h_flex() - .items_center() - .gap_2() - .child(Icon::new(icon).size_5().text_color(theme.foreground)) - .child( - div() - .font_bold() - .text_color(theme.foreground) - .child(title.to_string()), - ), - ) - .child(content), - ) - } - // Helper for Key-Value pairs fn render_kv( label: &str, @@ -115,70 +82,70 @@ impl HomeView { let flash_percent = (info.flash_used as f32 / info.flash_total as f32) * 100.0; - Self::home_card( - "Device Information", - Icon::default().path("icons/cpu.svg"), - v_flex() - .gap_6() - .child( - div() - .grid() - .grid_cols(2) - .gap_4() - .child(Self::render_kv( - "Serial Number", - info.serial.clone(), - theme, - true, - )) - .child(Self::render_kv( - "Firmware Version", - format!("v{}", info.firmware_version), - theme, - true, - )) - .child(Self::render_kv( - "VID:PID", - format!("{}, {}", config.vid, config.pid), - theme, - true, - )) - .child(Self::render_kv( - "Product Name", - config.product_name.clone(), - theme, - false, - )), - ) - .child(div().h_px().bg(theme.border)) - .child( - v_flex() - .gap_2() - .child( - h_flex() - .justify_between() - .text_sm() - .child( - div() - .text_color(theme.muted_foreground) - .child("Flash Memory"), - ) - .child(div().text_color(theme.foreground).child(format!( - "{:.0} / {:.0} KB", - info.flash_used, info.flash_total - ))), - ) - .child(Progress::new().value(flash_percent)), - ), - theme, - ) + Card::new() + .title("Device Information") + .icon(Icon::default().path("icons/cpu.svg")) + .child( + v_flex() + .gap_6() + .child( + div() + .grid() + .grid_cols(2) + .gap_4() + .child(Self::render_kv( + "Serial Number", + info.serial.clone(), + theme, + true, + )) + .child(Self::render_kv( + "Firmware Version", + format!("v{}", info.firmware_version), + theme, + true, + )) + .child(Self::render_kv( + "VID:PID", + format!("{}, {}", config.vid, config.pid), + theme, + true, + )) + .child(Self::render_kv( + "Product Name", + config.product_name.clone(), + theme, + false, + )), + ) + .child(div().h_px().bg(theme.border)) + .child( + v_flex() + .gap_2() + .child( + h_flex() + .justify_between() + .text_sm() + .child( + div() + .text_color(theme.muted_foreground) + .child("Flash Memory"), + ) + .child(div().text_color(theme.foreground).child(format!( + "{:.0} / {:.0} KB", + info.flash_used, info.flash_total + ))), + ) + .child(Progress::new().value(flash_percent)), + ), + ) } fn render_fido_info(state: &GlobalDeviceState, theme: &Theme) -> impl IntoElement { - Self::home_card( - "FIDO2 Information", - Icon::default().path("icons/shield.svg"), - if let Some(fido) = &state.fido_info { + Card::new() + .title("FIDO2 Information") + .icon(Icon::default().path("icons/shield.svg")) + .child(if let Some(fido) = &state.fido_info { v_flex() .gap_6() .child( @@ -228,18 +195,16 @@ impl HomeView { .text_color(theme.muted_foreground) .child("FIDO information not available") .into_any_element() - }, - theme, - ) + }) } fn render_led_config(state: &GlobalDeviceState, theme: &Theme) -> impl IntoElement { let status = state.device_status.as_ref().unwrap(); let config = &status.config; - Self::home_card( - "LED Configuration", - Icon::default().path("icons/microchip.svg"), - if status.method == DeviceMethod::Fido { + Card::new() + .title("LED Configuration") + .icon(Icon::default().path("icons/microchip.svg")) + .child(if status.method == DeviceMethod::Fido { v_flex() .items_center() .justify_center() @@ -328,95 +293,93 @@ impl HomeView { ), ) .into_any_element() - }, - theme, - ) + }) } fn render_security_status(state: &GlobalDeviceState, theme: &Theme) -> impl IntoElement { let status = state.device_status.as_ref().unwrap(); - Self::home_card( - "Security Status", - Icon::default().path("icons/shield-check.svg"), - v_flex() - .gap_3() - .text_sm() - .child( - h_flex() - .justify_between() - .items_center() - .child(div().text_color(theme.muted_foreground).child("Boot Mode")) - .child( - h_flex() - .gap_2() - .items_center() - .child(if status.secure_boot { - Icon::default() - .path("icons/lock.svg") - .size_3p5() - .text_color(gpui::green()) + Card::new() + .title("Security Status") + .icon(Icon::default().path("icons/shield-check.svg")) + .child( + v_flex() + .gap_3() + .text_sm() + .child( + h_flex() + .justify_between() + .items_center() + .child(div().text_color(theme.muted_foreground).child("Boot Mode")) + .child( + h_flex() + .gap_2() + .items_center() + .child(if status.secure_boot { + Icon::default() + .path("icons/lock.svg") + .size_3p5() + .text_color(gpui::green()) + } else { + Icon::default() + .path("icons/lock-open.svg") + .size_3p5() + .text_color(rgb(0xfe9a00)) + }) + .child( + Badge::new() + .child(if status.secure_boot { + "Secure Boot" + } else { + "Development" + }) + .color(if status.secure_boot { + theme.primary + } else { + theme.secondary + }), + ), + ), + ) + .child( + h_flex() + .justify_between() + .items_center() + .child( + div() + .text_color(theme.muted_foreground) + .child("Debug Interface"), + ) + .child(div().font_medium().text_color(theme.foreground).child( + if status.secure_lock { + "Read-out Locked" } else { - Icon::default() - .path("icons/lock-open.svg") - .size_3p5() - .text_color(rgb(0xfe9a00)) - }) - .child( - Badge::new() - .child(if status.secure_boot { - "Secure Boot" - } else { - "Development" - }) - .color(if status.secure_boot { - theme.primary - } else { - theme.secondary - }), - ), - ), - ) - .child( - h_flex() - .justify_between() - .items_center() - .child( - div() - .text_color(theme.muted_foreground) - .child("Debug Interface"), - ) - .child(div().font_medium().text_color(theme.foreground).child( - if status.secure_lock { - "Read-out Locked" - } else { - "Debug Enabled" - }, - )), - ) - .child( - h_flex() - .justify_between() - .items_center() - .child( - div() - .text_color(theme.muted_foreground) - .child("Secure Lock"), - ) - .child( - Badge::new() - .child(if status.secure_lock { - "Acknowledged" - } else { - "Pending" - }) - .color(if status.secure_lock { - gpui::red() - } else { - theme.secondary - }), - ), - ), - theme, - ) + "Debug Enabled" + }, + )), + ) + .child( + h_flex() + .justify_between() + .items_center() + .child( + div() + .text_color(theme.muted_foreground) + .child("Secure Lock"), + ) + .child( + Badge::new() + .child(if status.secure_lock { + "Acknowledged" + } else { + "Pending" + }) + .color(if status.secure_lock { + gpui::red() + } else { + theme.secondary + }), + ), + ), + ) } } diff --git a/src/ui/views/logs.rs b/src/ui/views/logs.rs index 7ef2444..40f83f8 100644 --- a/src/ui/views/logs.rs +++ b/src/ui/views/logs.rs @@ -1,7 +1,7 @@ use crate::ui::components::page_view::PageView; use gpui::*; use gpui_component::{ - ActiveTheme, Icon, StyledExt, button::Button, h_flex, scroll::ScrollableElement, v_flex, + ActiveTheme, Icon, button::Button, h_flex, scroll::ScrollableElement, v_flex, }; #[derive(Clone, Copy, Debug, PartialEq)] diff --git a/src/ui/views/passkeys.rs b/src/ui/views/passkeys.rs index 157783e..ec34489 100644 --- a/src/ui/views/passkeys.rs +++ b/src/ui/views/passkeys.rs @@ -1,4 +1,4 @@ -use crate::ui::components::page_view::PageView; +use crate::ui::components::{card::Card, page_view::PageView}; use gpui::*; use gpui_component::StyledExt; use gpui_component::button::ButtonVariants; @@ -125,75 +125,19 @@ impl PasskeysView { .into_any_element() } - fn main_card( - title: &str, - icon_path: impl Into, - description: &str, - content: impl IntoElement, - header_right: Option, - theme: &Theme, - ) -> impl IntoElement { - div() - .w_full() - .bg(rgb(0x18181b)) // Using the same dark bg as in HomeView - .border_1() - .border_color(theme.border) - .rounded_xl() - .p_6() + fn render_pin_management(device: &DeviceState, theme: &Theme) -> impl IntoElement { + Card::new() + .title("PIN Management") + .icon(Icon::default().path("icons/key.svg")) + .description("Configure FIDO2 PIN security") .child( v_flex() - .gap_6() - .child( - h_flex() - .items_center() - .justify_between() - .child( - v_flex() - .gap_1() - .child( - h_flex() - .items_center() - .gap_2() - .child( - Icon::default() - .path(icon_path) - .size_5() - .text_color(theme.foreground), - ) - .child( - div() - .font_bold() - .text_color(theme.foreground) - .child(title.to_string()), - ), - ) - .child( - div() - .text_sm() - .text_color(theme.muted_foreground) - .child(description.to_string()), - ), - ) - .children(header_right), - ) - .child(content), + .gap_4() + .child(Self::render_pin_status_row(device, theme)) + .child(Self::render_min_pin_length_row(device, theme)), ) } - fn render_pin_management(device: &DeviceState, theme: &Theme) -> impl IntoElement { - Self::main_card( - "PIN Management", - "icons/key.svg", - "Configure FIDO2 PIN security", - v_flex() - .gap_4() - .child(Self::render_pin_status_row(device, theme)) - .child(Self::render_min_pin_length_row(device, theme)), - None::, - theme, - ) - } - fn render_pin_status_row(device: &DeviceState, theme: &Theme) -> impl IntoElement { let pin_set = device .fido_info @@ -263,24 +207,22 @@ impl PasskeysView { } fn render_stored_passkeys(device: &DeviceState, theme: &Theme) -> impl IntoElement { - Self::main_card( - "Stored Passkeys", - "icons/key-round.svg", - "View and manage your resident credentials", - if !device.unlocked { + Card::new() + .title("Stored Passkeys") + .icon(Icon::default().path("icons/key-round.svg")) + .description("View and manage your resident credentials") + .child(if !device.unlocked { Self::render_locked_state(theme).into_any_element() } else { Self::render_unlocked_state(device, theme).into_any_element() - }, - Some( - div() - .child("View and manage your resident credentials") - .text_sm() - .text_color(theme.muted_foreground) - .invisible(), - ), - theme, - ) + }) + // header_right was effectively invisible/placeholder in original code: + // Some(div().child("View and manage...").invisible()) + // We can omit it or add it if needed, but it looked like a hack or mistake in original code? + // The original header_right was: + // Some(div().child("View and manage your resident credentials").text_sm().text_color(...).invisible()) + // This suggests it was maybe trying to take up space or something? But description handles the text. + // I will simplify and omit it, which is cleaner. } fn render_locked_state(theme: &Theme) -> impl IntoElement {