refactor: extract screens into view/view_model dirs, add AppModels DI, switch to Entity<DeviceRepo>

- Decompose flat about.rs/home.rs/security.rs/config.rs/passkeys.rs into
  per-screen directories with mod.rs, view.rs, view_model.rs
- Extract ApplicationRoot, LayoutState, ActiveView, ViewModelStore into new app.rs
- Replace DeviceConnectionState (snapshot-based) with reactive Entity<DeviceRepo>
- Replace ViewCache with ViewModelStore using uniform get_or_insert_with lazy init
- Add AppModels DI bag shared across all view constructors
- Move side types (UsbIdentityPreset, LedDriverType, etc.) into local view_model files
- Delete src/ui/types.rs
This commit is contained in:
Suyog Tandel
2026-07-05 16:02:14 +05:30
parent 7688cc3b13
commit 5fede906b0
25 changed files with 3668 additions and 3600 deletions
+2 -2
View File
@@ -819,7 +819,7 @@ use std::rc::Rc;
use gpui::*;
use gpui_component::Root;
use gpui_component::{Theme, ThemeMode, ThemeSet};
use ui::rootview::ApplicationRoot;
use ui::app::ApplicationRoot;
pub mod error;
mod hal;
@@ -837,7 +837,7 @@ fn main() {
// Register sidebar toggle keybinding
cx.bind_keys([gpui::KeyBinding::new(
"ctrl-shift-d",
ui::rootview::ToggleSidebar,
ui::app::ToggleSidebar,
None,
)]);
+170
View File
@@ -0,0 +1,170 @@
use crate::hal::io;
use crate::hal::types::{DeviceMethod, FirmwareType};
use crate::ui::models::device::DeviceRepo;
use crate::ui::screens::{
about::AboutViewModel, config::ConfigView, home::HomeViewModel, passkeys::PasskeysView,
security::SecurityViewModel,
};
use gpui::*;
gpui::actions!(picoforge, [ToggleSidebar]);
pub struct AppModels {
pub device: Entity<DeviceRepo>,
}
pub struct ViewModelStore {
pub home: Option<Entity<HomeViewModel>>,
pub about: Option<Entity<AboutViewModel>>,
pub security: Option<Entity<SecurityViewModel>>,
pub passkeys: Option<Entity<PasskeysView>>,
pub config: Option<Entity<ConfigView>>,
}
impl ViewModelStore {
pub fn new() -> Self {
Self {
home: None,
about: None,
security: None,
passkeys: None,
config: None,
}
}
}
#[derive(Clone, Copy, PartialEq, Debug)]
pub enum ActiveView {
Home,
Passkeys,
Configuration,
Security,
About,
}
#[derive(Clone, Debug, PartialEq)]
pub struct LayoutState {
pub active_view: ActiveView,
pub is_sidebar_collapsed: bool,
pub sidebar_toggle_hovered: bool,
pub sidebar_width: Pixels,
}
impl LayoutState {
pub fn new() -> Self {
Self {
active_view: ActiveView::Home,
is_sidebar_collapsed: false,
sidebar_toggle_hovered: false,
sidebar_width: px(255.),
}
}
}
pub struct ApplicationRoot {
pub models: AppModels,
pub view_state: LayoutState,
pub views_store: ViewModelStore,
pub focus_handle: FocusHandle,
}
impl ApplicationRoot {
pub fn new(cx: &mut Context<Self>) -> Self {
let device = cx.new(|_| DeviceRepo::new());
let mut this = Self {
models: AppModels { device },
view_state: LayoutState::new(),
views_store: ViewModelStore::new(),
focus_handle: cx.focus_handle(),
};
this.refresh_device_status(None, cx);
this
}
pub fn focus_handle(&self) -> FocusHandle {
self.focus_handle.clone()
}
pub(crate) fn refresh_device_status(
&mut self,
window: Option<&mut Window>,
cx: &mut Context<Self>,
) {
if self.models.device.read(cx).is_loading() {
return;
}
self.models.device.update(cx, |repo, _| repo.begin_load());
cx.notify();
match io::read_device_details() {
Ok(status) => {
let device_changed = self
.models
.device
.update(cx, |repo, _| repo.set_status(status.clone()));
let firmware_type = status.firmware_type;
let method = status.method;
if device_changed {
self.views_store.passkeys = None;
} else if let Some(passkeys_view) = &self.views_store.passkeys {
passkeys_view.update(cx, |view, cx| {
view.refresh_if_unlocked(cx);
});
}
match io::get_fido_info() {
Ok(fido) => {
self.models
.device
.update(cx, |repo, _| repo.set_fido_info(Some(fido)));
}
Err(e) => {
log::error!("FIDO Info fetch failed: {}", e);
self.models
.device
.update(cx, |repo, _| repo.set_fido_info(None));
}
}
if firmware_type == FirmwareType::RSKey && method == DeviceMethod::Rescue {
let led = io::read_led_config().ok();
let mgmt = io::read_management_config().ok();
self.models
.device
.update(cx, |repo, _| repo.set_auxiliary_data(led, mgmt));
} else {
self.models
.device
.update(cx, |repo, _| repo.clear_auxiliary_data());
}
if let Some(config_view) = &self.views_store.config
&& let Some(window) = window
{
config_view.update(cx, |view, cx| {
view.sync_from_device(window, cx);
});
}
}
Err(e) => {
self.models
.device
.update(cx, |repo, _| repo.set_error(format!("{}", e)));
self.views_store.passkeys = None;
}
}
self.models.device.update(cx, |repo, _| repo.end_load());
cx.notify();
}
pub fn toggle_sidebar(&mut self, cx: &mut Context<Self>) {
self.view_state.is_sidebar_collapsed = !self.view_state.is_sidebar_collapsed;
cx.notify();
}
}
+12 -9
View File
@@ -1,6 +1,7 @@
use crate::hal::types::DeviceMethod;
use crate::ui::app::{ActiveView, AppModels};
use crate::ui::components::button::PFIconButton;
use crate::ui::types::{ActiveView, DeviceConnectionState};
use crate::ui::models::device::DeviceRepo;
use gpui::*;
use gpui_component::{
ActiveTheme, Icon, IconName, Side,
@@ -18,7 +19,7 @@ pub struct AppSidebar<V: 'static> {
active_view: ActiveView,
width: Pixels,
collapsed: bool,
state: DeviceConnectionState,
device: Entity<DeviceRepo>,
on_select: SelectHandler<V>,
on_refresh: RefreshHandler<V>,
}
@@ -28,13 +29,13 @@ impl<V: 'static> AppSidebar<V> {
active_view: ActiveView,
width: Pixels,
collapsed: bool,
state: DeviceConnectionState,
models: &AppModels,
) -> Self {
Self {
active_view,
width,
collapsed,
state,
device: models.device.clone(),
on_select: None,
on_refresh: None,
}
@@ -59,7 +60,9 @@ impl<V: 'static> AppSidebar<V> {
pub fn render(self, cx: &mut Context<V>) -> impl IntoElement {
let width = self.width;
let collapsed = self.collapsed;
let state = self.state.clone();
let state = self.device.read(cx);
let status_owned = state.status.clone();
let error_owned = state.error.clone();
let sidebar_bg = cx.theme().sidebar;
let sidebar_fg = cx.theme().sidebar_foreground;
@@ -191,13 +194,13 @@ impl<V: 'static> AppSidebar<V> {
.w_full(),
)
.child(div().w(px(8.)).h(px(8.)).rounded_full().bg(
if let Some(status) = &state.status {
if let Some(status) = &status_owned {
if status.method == DeviceMethod::Fido {
rgb(0xf59e0b)
} else {
rgb(0x22c55e)
}
} else if state.error.is_some() {
} else if error_owned.is_some() {
rgb(0xf59e0b)
} else {
rgb(0xef4444)
@@ -220,7 +223,7 @@ impl<V: 'static> AppSidebar<V> {
)
.child({
let (text, color_bg, color_text) = if let Some(status) =
&state.status
&status_owned
{
let is_rskey = status.firmware_type
== crate::hal::types::FirmwareType::RSKey;
@@ -240,7 +243,7 @@ impl<V: 'static> AppSidebar<V> {
rgb(0xffffff),
)
}
} else if state.error.is_some() {
} else if error_owned.is_some() {
("Error".to_string(), rgb(0xd97706), rgb(0xffffff))
} else {
("Offline".to_string(), rgb(0xef4444), rgb(0xffffff))
+1 -1
View File
@@ -1,7 +1,7 @@
pub mod app;
pub mod assets;
pub mod colors;
pub mod components;
pub mod models;
pub mod rootview;
pub mod screens;
pub mod types;
+55 -2
View File
@@ -1,14 +1,13 @@
use crate::hal::types::{FidoDeviceInfo, FullDeviceStatus, LedStatusConfig, ManagementAppConfig};
use gpui::*;
// Repo Events:
#[allow(dead_code)]
pub enum DeviceEvent {
Updated,
}
impl EventEmitter<DeviceEvent> for DeviceRepo {}
// Repo Definition:
pub struct DeviceRepo {
pub status: Option<FullDeviceStatus>,
pub fido_info: Option<FidoDeviceInfo>,
@@ -29,4 +28,58 @@ impl DeviceRepo {
loading: false,
}
}
// --- State lifecycle ---
pub fn begin_load(&mut self) {
self.loading = true;
self.error = None;
}
pub fn end_load(&mut self) {
self.loading = false;
}
pub fn is_loading(&self) -> bool {
self.loading
}
// --- Field setters ---
pub fn set_status(&mut self, status: FullDeviceStatus) -> bool {
let changed = self
.status
.as_ref()
.map(|s| s.info.serial != status.info.serial)
.unwrap_or(true);
self.status = Some(status);
changed
}
pub fn set_fido_info(&mut self, fido: Option<FidoDeviceInfo>) {
self.fido_info = fido;
}
pub fn set_auxiliary_data(
&mut self,
led: Option<LedStatusConfig>,
mgmt: Option<ManagementAppConfig>,
) {
self.led_status = led;
self.management_apps = mgmt;
}
pub fn clear_auxiliary_data(&mut self) {
self.led_status = None;
self.management_apps = None;
}
pub fn set_error(&mut self, error: String) {
self.status = None;
self.fido_info = None;
self.led_status = None;
self.management_apps = None;
self.loading = false;
self.error = Some(error);
}
}
+1 -1
View File
@@ -1 +1 @@
mod device;
pub mod device;
+40 -137
View File
@@ -1,10 +1,9 @@
use crate::hal::io;
use crate::ui::app::{ActiveView, ApplicationRoot, ToggleSidebar};
use crate::ui::components::sidebar::AppSidebar;
use crate::ui::screens::{
about::AboutView, config::ConfigView, home::HomeView, passkeys::PasskeysEvent,
passkeys::PasskeysView, security::SecurityView,
about::AboutViewModel, config::ConfigView, home::HomeViewModel, passkeys::PasskeysEvent,
passkeys::PasskeysView, security::SecurityViewModel,
};
use crate::ui::types::{ActiveView, DeviceConnectionState, LayoutState, ViewCache};
use gpui::prelude::*;
use gpui::*;
use gpui_component::Root;
@@ -12,114 +11,11 @@ use gpui_component::{
ActiveTheme, Icon, TitleBar, WindowExt, h_flex, scroll::ScrollableElement, v_flex,
};
gpui::actions!(picoforge, [ToggleSidebar]);
pub struct ApplicationRoot {
pub device: DeviceConnectionState,
pub layout: LayoutState,
pub views: ViewCache,
pub focus_handle: FocusHandle,
}
impl ApplicationRoot {
pub fn new(cx: &mut Context<Self>) -> Self {
let mut this = Self {
device: DeviceConnectionState::new(),
layout: LayoutState::new(),
views: ViewCache::new(),
focus_handle: cx.focus_handle(),
};
this.refresh_device_status(None, cx);
this
}
pub fn focus_handle(&self) -> FocusHandle {
self.focus_handle.clone()
}
fn refresh_device_status(&mut self, window: Option<&mut Window>, cx: &mut Context<Self>) {
if self.device.loading {
return;
}
self.device.loading = true;
self.device.error = None;
cx.notify();
match io::read_device_details() {
Ok(status) => {
let device_changed = self
.device
.status
.as_ref()
.map(|s| s.info.serial != status.info.serial)
.unwrap_or(true);
self.device.status = Some(status.clone());
self.device.error = None;
if device_changed {
self.views.passkeys = None;
} else if let Some(passkeys_view) = &self.views.passkeys {
passkeys_view.update(cx, |view, cx| {
view.refresh_if_unlocked(cx);
});
}
match io::get_fido_info() {
Ok(fido) => {
self.device.fido_info = Some(fido);
}
Err(e) => {
log::error!("FIDO Info fetch failed: {}", e);
self.device.fido_info = None;
}
}
if status.firmware_type == crate::hal::types::FirmwareType::RSKey
&& status.method == crate::hal::types::DeviceMethod::Rescue
{
self.device.led_status = io::read_led_config().ok();
self.device.management_apps = io::read_management_config().ok();
} else {
self.device.led_status = None;
self.device.management_apps = None;
}
if let Some(config_view) = &self.views.config
&& let Some(window) = window
{
let device = self.device.clone();
config_view.update(cx, |view, cx| {
view.sync_from_device(&device, window, cx);
});
}
}
Err(e) => {
self.device.status = None;
self.device.error = Some(format!("{}", e));
self.device.fido_info = None;
self.device.led_status = None;
self.device.management_apps = None;
// Drop cached passkeys view (and cached PIN) on disconnect.
self.views.passkeys = None;
}
}
self.device.loading = false;
cx.notify();
}
pub fn toggle_sidebar(&mut self, cx: &mut Context<Self>) {
self.layout.is_sidebar_collapsed = !self.layout.is_sidebar_collapsed;
cx.notify();
}
}
impl Render for ApplicationRoot {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let window_width = window.bounds().size.width;
let is_window_wide = window_width > px(800.0);
let is_sidebar_collapsed = self.layout.is_sidebar_collapsed || !is_window_wide;
let is_sidebar_collapsed = self.view_state.is_sidebar_collapsed || !is_window_wide;
let target_width = if is_sidebar_collapsed {
px(48.)
@@ -127,12 +23,12 @@ impl Render for ApplicationRoot {
px(255.)
};
if (self.layout.sidebar_width - target_width).abs() > px(0.1) {
self.layout.sidebar_width =
self.layout.sidebar_width + (target_width - self.layout.sidebar_width) * 0.2;
if (self.view_state.sidebar_width - target_width).abs() > px(0.1) {
self.view_state.sidebar_width = self.view_state.sidebar_width
+ (target_width - self.view_state.sidebar_width) * 0.2;
window.request_animation_frame();
} else {
self.layout.sidebar_width = target_width;
self.view_state.sidebar_width = target_width;
}
let dialog_layer = Root::render_dialog_layer(window, cx);
@@ -158,15 +54,16 @@ impl Render for ApplicationRoot {
.overflow_y_scrollbar()
.flex_grow()
.bg(cx.theme().background)
.child(match self.layout.active_view {
.child(match self.view_state.active_view {
ActiveView::Home => {
HomeView::build(&self.device, cx.theme(), window.bounds().size.width)
.into_any_element()
let view = self.views_store.home.get_or_insert_with(|| {
cx.new(|cx| HomeViewModel::new(window, cx, &self.models))
});
view.clone().into_any_element()
}
ActiveView::Passkeys => {
let view = self.views.passkeys.get_or_insert_with(|| {
let root = cx.entity().downgrade();
let view = cx.new(|cx| PasskeysView::new(window, cx, root));
let view = self.views_store.passkeys.get_or_insert_with(|| {
let view = cx.new(|cx| PasskeysView::new(window, cx, &self.models));
cx.subscribe_in(
&view,
window,
@@ -182,38 +79,43 @@ impl Render for ApplicationRoot {
view.clone().into_any_element()
}
ActiveView::Configuration => {
if self.views.config.is_none() {
let root = cx.entity().downgrade();
let device = self.device.clone();
self.views.config =
Some(cx.new(|cx| ConfigView::new(window, cx, root, device)));
}
self.views.config.clone().unwrap().into_any_element()
let view = self.views_store.config.get_or_insert_with(|| {
cx.new(|cx| ConfigView::new(window, cx, &self.models))
});
view.clone().into_any_element()
}
ActiveView::Security => {
let view = self.views_store.security.get_or_insert_with(|| {
cx.new(|cx| SecurityViewModel::new(window, cx, &self.models))
});
view.clone().into_any_element()
}
ActiveView::About => {
let view = self.views_store.about.get_or_insert_with(|| {
cx.new(|cx| AboutViewModel::new(window, cx, &self.models))
});
view.clone().into_any_element()
}
ActiveView::Security => SecurityView::build(cx).into_any_element(),
ActiveView::About => AboutView::build(cx.theme()).into_any_element(),
});
let sidebar = AppSidebar::new(
self.layout.active_view,
self.layout.sidebar_width,
self.view_state.active_view,
self.view_state.sidebar_width,
is_sidebar_collapsed,
self.device.clone(),
&self.models,
)
.on_select(|this: &mut Self, view, _, _| {
this.layout.active_view = view;
this.view_state.active_view = view;
})
.on_refresh(|this, window, cx| {
this.refresh_device_status(Some(window), cx);
});
// Toggle button absolutely positioned at the sidebar's right edge.
// It fades in on hover when the sidebar is collapsed.
let sidebar_bg = cx.theme().sidebar;
let border_color = cx.theme().sidebar_border;
let sidebar_fg = cx.theme().sidebar_foreground;
let is_toggle_visible = !is_sidebar_collapsed || self.layout.sidebar_toggle_hovered;
let sidebar_width = self.layout.sidebar_width;
let is_toggle_visible = !is_sidebar_collapsed || self.view_state.sidebar_toggle_hovered;
let sidebar_width = self.view_state.sidebar_width;
let toggle_icon = if is_sidebar_collapsed {
"icons/chevron-right.svg"
} else {
@@ -235,7 +137,7 @@ impl Render for ApplicationRoot {
.items_center()
.justify_center()
.on_hover(cx.listener(|this, hovered, _, cx| {
this.layout.sidebar_toggle_hovered = *hovered;
this.view_state.sidebar_toggle_hovered = *hovered;
cx.notify();
}))
.child(
@@ -258,7 +160,8 @@ impl Render for ApplicationRoot {
.build(window, cx)
})
.on_click(cx.listener(|this, _, _, _| {
this.layout.is_sidebar_collapsed = !this.layout.is_sidebar_collapsed;
this.view_state.is_sidebar_collapsed =
!this.view_state.is_sidebar_collapsed;
}))
.child(Icon::default().path(toggle_icon).text_color(sidebar_fg)),
);
+3
View File
@@ -0,0 +1,3 @@
pub mod view;
pub mod view_model;
pub use view_model::AboutViewModel;
@@ -1,12 +1,11 @@
// src/views/about.rs
use crate::ui::components::{card::Card, page_view::PageView, tag::Tag};
use crate::ui::screens::about::view_model::AboutViewModel;
use gpui::*;
use gpui_component::{Icon, StyledExt, Theme, button::Button, h_flex, v_flex};
use gpui_component::{ActiveTheme, Icon, StyledExt, button::Button, h_flex, v_flex};
pub struct AboutView;
impl AboutView {
pub fn build(theme: &Theme) -> impl IntoElement {
impl Render for AboutViewModel {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let theme = cx.theme();
PageView::build(
"About",
"Information about the application and its development.",
+10
View File
@@ -0,0 +1,10 @@
use crate::ui::app::AppModels;
use gpui::*;
pub struct AboutViewModel;
impl AboutViewModel {
pub fn new(_window: &mut Window, _cx: &mut Context<Self>, _models: &AppModels) -> Self {
Self
}
}
File diff suppressed because it is too large Load Diff
+3
View File
@@ -0,0 +1,3 @@
pub mod view;
pub mod view_model;
pub use view_model::ConfigView;
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+3
View File
@@ -0,0 +1,3 @@
pub mod view;
pub mod view_model;
pub use view_model::HomeViewModel;
@@ -1,59 +1,12 @@
use crate::hal::types::DeviceMethod;
use crate::hal::types::{DeviceMethod, FidoDeviceInfo, FullDeviceStatus};
use crate::ui::components::{card::Card, page_view::PageView, tag::Tag};
use crate::ui::types::DeviceConnectionState;
use crate::ui::screens::home::view_model::HomeViewModel;
use gpui::prelude::FluentBuilder;
use gpui::*;
use gpui_component::StyledExt;
use gpui_component::{ActiveTheme, StyledExt};
use gpui_component::{Icon, IconName, Theme, h_flex, progress::Progress, v_flex};
pub struct HomeView;
impl HomeView {
pub fn build(
state: &DeviceConnectionState,
theme: &Theme,
window_width: Pixels,
) -> impl IntoElement {
let connected = state.status.is_some();
let is_wide = window_width > px(1100.0);
let columns = if is_wide { 2 } else { 1 };
PageView::build(
"Device Overview",
"Quick view of your device status and specifications.",
if !connected {
// No Device Status Placeholder
div()
.flex()
.items_center()
.justify_center()
.h_64()
.border_1()
.border_color(theme.border)
.rounded_xl()
.child(
div()
.text_color(theme.muted_foreground)
.child("No Device Connected"),
)
.into_any_element()
} else {
// Card Grid
div()
.grid()
.grid_cols(columns)
.gap_6()
.child(Self::render_device_info(state, theme))
.child(Self::render_fido_info(state, theme))
.child(Self::render_led_config(state, theme))
.child(Self::render_security_status(state, theme))
.into_any_element()
},
theme,
)
}
// Helper for Key-Value pairs
impl HomeViewModel {
fn render_kv(
label: &str,
value: impl IntoElement,
@@ -82,8 +35,7 @@ impl HomeView {
)
}
fn render_device_info(state: &DeviceConnectionState, theme: &Theme) -> impl IntoElement {
let status = state.status.as_ref().unwrap();
fn render_device_info(status: &FullDeviceStatus, theme: &Theme) -> impl IntoElement {
let info = &status.info;
let config = &status.config;
@@ -165,15 +117,14 @@ impl HomeView {
)
}
fn render_fido_info(state: &DeviceConnectionState, theme: &Theme) -> impl IntoElement {
fn render_fido_info(fido: Option<&FidoDeviceInfo>, theme: &Theme) -> impl IntoElement {
Card::new()
.title("FIDO2 Information")
.icon(Icon::default().path("icons/shield.svg"))
.child(if let Some(fido) = &state.fido_info {
.child(if let Some(fido) = fido {
v_flex()
.gap_3()
.text_sm()
// AAGUID
.child(
h_flex()
.justify_between()
@@ -188,7 +139,6 @@ impl HomeView {
.child(fido.aaguid.clone()),
),
)
// FIDO Versions
.child(
h_flex()
.justify_between()
@@ -209,7 +159,6 @@ impl HomeView {
)),
)
.child(div().h_px().bg(theme.border))
// PIN Set
.child(
h_flex()
.justify_between()
@@ -221,7 +170,6 @@ impl HomeView {
Tag::new(if pin_set { "Set" } else { "Not Set" }).active(pin_set)
}),
)
// Resident Keys
.child(
h_flex()
.justify_between()
@@ -236,7 +184,6 @@ impl HomeView {
Tag::new(if rk { "Supported" } else { "Not Supported" }).active(rk)
}),
)
// Min PIN Length
.child(
h_flex()
.justify_between()
@@ -253,7 +200,6 @@ impl HomeView {
.child(fido.min_pin_length.to_string()),
),
)
// Enterprise attestation
.child(
h_flex()
.justify_between()
@@ -268,7 +214,6 @@ impl HomeView {
Tag::new(if ep_set { "Set" } else { "Not Set" }).active(ep_set)
})),
)
// Remaining Credentials
.when(fido.remaining_discoverable_credentials.is_some(), |this| {
this.child(
h_flex()
@@ -298,8 +243,7 @@ impl HomeView {
})
}
fn render_led_config(state: &DeviceConnectionState, theme: &Theme) -> impl IntoElement {
let status = state.status.as_ref().unwrap();
fn render_led_config(status: &FullDeviceStatus, theme: &Theme) -> impl IntoElement {
let config = &status.config;
Card::new()
.title("LED Configuration")
@@ -386,8 +330,7 @@ impl HomeView {
})
}
fn render_security_status(state: &DeviceConnectionState, theme: &Theme) -> impl IntoElement {
let status = state.status.as_ref().unwrap();
fn render_security_status(status: &FullDeviceStatus, theme: &Theme) -> impl IntoElement {
Card::new()
.title("Security Status")
.icon(Icon::default().path("icons/shield-check.svg"))
@@ -463,3 +406,48 @@ impl HomeView {
)
}
}
impl Render for HomeViewModel {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let device = self.device.read(cx);
let connected = device.status.is_some();
let is_wide = window.bounds().size.width > px(1100.0);
let columns = if is_wide { 2 } else { 1 };
PageView::build(
"Device Overview",
"Quick view of your device status and specifications.",
if !connected {
div()
.flex()
.items_center()
.justify_center()
.h_64()
.border_1()
.border_color(cx.theme().border)
.rounded_xl()
.child(
div()
.text_color(cx.theme().muted_foreground)
.child("No Device Connected"),
)
.into_any_element()
} else {
let status = device.status.as_ref().unwrap();
div()
.grid()
.grid_cols(columns)
.gap_6()
.child(Self::render_device_info(status, cx.theme()))
.child(Self::render_fido_info(
device.fido_info.as_ref(),
cx.theme(),
))
.child(Self::render_led_config(status, cx.theme()))
.child(Self::render_security_status(status, cx.theme()))
.into_any_element()
},
cx.theme(),
)
}
}
+16
View File
@@ -0,0 +1,16 @@
use crate::ui::app::AppModels;
use crate::ui::models::device::{DeviceEvent, DeviceRepo};
use gpui::*;
pub struct HomeViewModel {
pub device: Entity<DeviceRepo>,
}
impl HomeViewModel {
pub fn new(_window: &mut Window, cx: &mut Context<Self>, models: &AppModels) -> Self {
let device = models.device.clone();
cx.subscribe(&device, |_, _, _: &DeviceEvent, cx| cx.notify())
.detach();
Self { device }
}
}
File diff suppressed because it is too large Load Diff
+3
View File
@@ -0,0 +1,3 @@
pub mod view;
pub mod view_model;
pub use view_model::{PasskeysEvent, PasskeysView};
File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More