mirror of
https://github.com/librekeys/picoforge.git
synced 2026-07-28 08:01:19 -07:00
chore(docs): refactor and update the docs to point to correct dir locations and explain the new UI architecture.
- updated mod doc in main.rs - added mod doc in ui/mod.rs - added few documentation to symbols in app.rs
This commit is contained in:
+61
-43
@@ -91,18 +91,18 @@
|
||||
//!
|
||||
//! ### Where to Start
|
||||
//!
|
||||
//! - **New to the codebase?** Start with `src/main.rs` (this file) and `src/ui/rootview.rs`
|
||||
//! - **New to the codebase?** Start with `src/main.rs` (this file) and `src/ui/app.rs`
|
||||
//! to understand the application entry point and main window structure.
|
||||
//!
|
||||
//! - **Want to understand device communication?** Read `src/device/mod.rs` and
|
||||
//! `src/device/io.rs` for the high-level API, then dive into `src/device/rescue/mod.rs`
|
||||
//! (PC/SC protocol) or `src/device/fido/mod.rs` (FIDO2/CTAP2 protocol).
|
||||
//! - **Want to understand device communication?** Read `src/hal/mod.rs` and
|
||||
//! `src/hal/io.rs` for the high-level API, then dive into `src/hal/rescue/mod.rs`
|
||||
//! (PC/SC protocol) or `src/hal/fido/mod.rs` (FIDO2/CTAP2 protocol).
|
||||
//!
|
||||
//! - **Working on UI features?** Explore `src/ui/views/` for page implementations and
|
||||
//! `src/ui/components/` for reusable widgets. See `src/ui/types.rs` for state types.
|
||||
//! - **Working on UI features?** Explore `src/ui/screens/` for page implementations and
|
||||
//! `src/ui/components/` for reusable widgets. See `src/ui/models/` for shared state.
|
||||
//!
|
||||
//! - **Adding new device commands?** Check `src/device/fido/constants.rs` and
|
||||
//! `src/device/rescue/constants.rs` for protocol constants, then implement in the
|
||||
//! - **Adding new device commands?** Check `src/hal/fido/constants.rs` and
|
||||
//! `src/hal/rescue/constants.rs` for protocol constants, then implement in the
|
||||
//! appropriate protocol module.
|
||||
//!
|
||||
//! ---
|
||||
@@ -143,7 +143,7 @@
|
||||
//! │ ├── main.rs # ← THIS FILE: Application entry point
|
||||
//! │ ├── error.rs # Application-wide error types (PFError)
|
||||
//! │ ├── logging.rs # log4rs configuration
|
||||
//! │ ├── device/ # Hardware communication layer
|
||||
//! │ ├── hal/ # Hardware abstraction layer
|
||||
//! │ │ ├── mod.rs # Module root, re-exports
|
||||
//! │ │ ├── io.rs # High-level API bridging rescue and FIDO
|
||||
//! │ │ ├── types.rs # Shared data structures
|
||||
@@ -156,17 +156,34 @@
|
||||
//! │ │ └── hid.rs # USB HID transport
|
||||
//! │ └── ui/ # GPUI frontend
|
||||
//! │ ├── mod.rs
|
||||
//! │ ├── rootview.rs # Main window, sidebar routing
|
||||
//! │ ├── types.rs # UI state types
|
||||
//! │ ├── app.rs # ApplicationRoot, AppModels, layout, Render
|
||||
//! │ ├── assets.rs # rust-embed asset loader
|
||||
//! │ ├── colors.rs # Theme color constants
|
||||
//! │ ├── views/ # Page views (sidebar sections)
|
||||
//! │ ├── models/ # Shared reactive state (DeviceRepo)
|
||||
//! │ │ ├── mod.rs
|
||||
//! │ │ ├── home.rs
|
||||
//! │ │ ├── passkeys.rs
|
||||
//! │ │ ├── config.rs
|
||||
//! │ │ ├── security.rs
|
||||
//! │ │ └── about.rs
|
||||
//! │ │ └── device.rs
|
||||
//! │ ├── screens/ # Page views (sidebar sections)
|
||||
//! │ │ ├── mod.rs
|
||||
//! │ │ ├── home/
|
||||
//! │ │ │ ├── mod.rs
|
||||
//! │ │ │ ├── view.rs
|
||||
//! │ │ │ └── view_model.rs
|
||||
//! │ │ ├── passkeys/
|
||||
//! │ │ │ ├── mod.rs
|
||||
//! │ │ │ ├── view.rs
|
||||
//! │ │ │ └── view_model.rs
|
||||
//! │ │ ├── config/
|
||||
//! │ │ │ ├── mod.rs
|
||||
//! │ │ │ ├── view.rs
|
||||
//! │ │ │ └── view_model.rs
|
||||
//! │ │ ├── security/
|
||||
//! │ │ │ ├── mod.rs
|
||||
//! │ │ │ ├── view.rs
|
||||
//! │ │ │ └── view_model.rs
|
||||
//! │ │ └── about/
|
||||
//! │ │ ├── mod.rs
|
||||
//! │ │ ├── view.rs
|
||||
//! │ │ └── view_model.rs
|
||||
//! │ └── components/ # Reusable UI widgets
|
||||
//! │ ├── mod.rs
|
||||
//! │ ├── button.rs
|
||||
@@ -257,22 +274,22 @@
|
||||
//! ┌─────────────────────────────────────────────────────────────┐
|
||||
//! │ UI Layer (src/ui/) │
|
||||
//! │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
|
||||
//! │ │ HomeView │ │Passkeys │ │ConfigView│ │Security │ │
|
||||
//! │ │ │ │View │ │ │ │View │ │
|
||||
//! │ │ HomeView │ │Passkeys │ │ConfigVM │ │Security │ │
|
||||
//! │ │ │ │ViewModel │ │ │ │View │ │
|
||||
//! │ └────┬─────┘ └─────┬────┘ └──────┬───┘ └───────┬──┘ │
|
||||
//! │ │ │ │ │ │
|
||||
//! │ └──────────────┴──────┬───────┴──────────────┘ │
|
||||
//! │ │ │
|
||||
//! │ ┌────────▼────────┐ │
|
||||
//! │ │ ApplicationRoot │ (rootview.rs) │
|
||||
//! │ │ DeviceState │ │
|
||||
//! │ │ ApplicationRoot │ (app.rs) │
|
||||
//! │ │ AppModels │ │
|
||||
//! │ │ LayoutState │ │
|
||||
//! │ │ ViewCache │ │
|
||||
//! │ │ ViewModelStore │ │
|
||||
//! │ └────────┬────────┘ │
|
||||
//! └─────────────────────────────┼───────────────────────────────┘
|
||||
//! │
|
||||
//! ┌─────────────────────────────▼───────────────────────────────┐
|
||||
//! │ Device I/O Layer (src/device/io.rs) │
|
||||
//! │ Hardware I/O Layer (src/hal/io.rs) │
|
||||
//! │ High-level API: read_device_details() │
|
||||
//! │ write_config() │
|
||||
//! │ get_credentials() │
|
||||
@@ -283,7 +300,7 @@
|
||||
//! │ │
|
||||
//! ┌────────────▼────────────┐ ┌─────────────────▼─────────────┐
|
||||
//! │ Rescue Protocol │ │ FIDO2 Protocol │
|
||||
//! │ (src/device/rescue/) │ │ (src/device/fido/) │
|
||||
//! │ (src/hal/rescue/) │ │ (src/hal/fido/) │
|
||||
//! │ │ │ │
|
||||
//! │ PC/SC + ISO 7816-4 │ │ CTAPHID + CTAP2 │
|
||||
//! │ APDU commands │ │ CBOR messages │
|
||||
@@ -303,8 +320,8 @@
|
||||
//!
|
||||
//! ### Key Design Principles
|
||||
//!
|
||||
//! 1. **Separation of Concerns**: Device communication (`src/device/`) is completely
|
||||
//! separate from UI code (`src/ui/`). No GPUI imports exist in device modules.
|
||||
//! 1. **Separation of Concerns**: Device communication (`src/hal/`) is completely
|
||||
//! separate from UI code (`src/ui/`). No GPUI imports exist in HAL modules.
|
||||
//!
|
||||
//! 2. **Protocol Abstraction**: The `io.rs` layer provides a unified API that
|
||||
//! automatically selects Rescue or FIDO2 based on device capabilities and
|
||||
@@ -314,8 +331,8 @@
|
||||
//! and RS-Key (Rust firmware) with the same UI, detecting firmware type via
|
||||
//! AAGUID and adapting behavior accordingly.
|
||||
//!
|
||||
//! 4. **State-Driven UI**: The `ApplicationRoot` maintains device state and
|
||||
//! propagates changes to views via the `DeviceConnectionState` struct.
|
||||
//! 4. **State-Driven UI**: The `ApplicationRoot` holds `AppModels` with a reactive
|
||||
//! `Entity<DeviceRepo>` that views subscribe to via `DeviceEvent::Updated`.
|
||||
//!
|
||||
//! ---
|
||||
//!
|
||||
@@ -346,7 +363,7 @@
|
||||
//! └── Vendor commands (0xC1/0xC2)
|
||||
//!
|
||||
//! ▼
|
||||
//! Update DeviceConnectionState
|
||||
//! Update DeviceRepo
|
||||
//! │
|
||||
//! ├── status: FullDeviceStatus
|
||||
//! ├── fido_info: FidoDeviceInfo
|
||||
@@ -360,7 +377,7 @@
|
||||
//! ### Configuration Write Flow
|
||||
//!
|
||||
//! ```text
|
||||
//! User edits config form (ConfigView)
|
||||
//! User edits config form (ConfigViewModel)
|
||||
//! │
|
||||
//! ▼
|
||||
//! io::write_config(config, method, pin)
|
||||
@@ -387,7 +404,7 @@
|
||||
//! ### Credential Management Flow
|
||||
//!
|
||||
//! ```text
|
||||
//! PasskeysView::unlock_storage(pin)
|
||||
//! PasskeysViewModel::unlock_storage(pin)
|
||||
//! │
|
||||
//! ▼
|
||||
//! io::get_credentials(pin)
|
||||
@@ -407,7 +424,7 @@
|
||||
//! Parse CBOR responses → Vec<StoredCredential>
|
||||
//! │
|
||||
//! ▼
|
||||
//! Display credentials in PasskeysView table
|
||||
//! Display credentials in PasskeysViewModel table
|
||||
//! ```
|
||||
//!
|
||||
//! ---
|
||||
@@ -503,9 +520,9 @@
|
||||
//! ```text
|
||||
//! ApplicationRoot::new(cx)
|
||||
//! │
|
||||
//! ├── DeviceConnectionState::new()
|
||||
//! ├── AppModels { device: Entity<DeviceRepo> }
|
||||
//! ├── LayoutState::new()
|
||||
//! ├── ViewCache::new()
|
||||
//! ├── ViewModelStore::new() [all None, lazy-init]
|
||||
//! │
|
||||
//! └── refresh_device_status() → io::read_device_details()
|
||||
//!
|
||||
@@ -515,21 +532,22 @@
|
||||
//! │ └── on_select → updates LayoutState.active_view
|
||||
//! │
|
||||
//! └── Render content based on active_view:
|
||||
//! ├── Home → HomeView::build() [stateless]
|
||||
//! ├── Passkeys → PasskeysView::new() [stateful, cached]
|
||||
//! ├── Configuration → ConfigView::new() [stateful, cached]
|
||||
//! ├── Security → SecurityView::build() [stateless]
|
||||
//! └── About → AboutView::build() [stateless]
|
||||
//! ├── Home → HomeViewModel::new() [lazy, cached]
|
||||
//! ├── Passkeys → PasskeysViewModel::new() [lazy, cached]
|
||||
//! ├── Configuration → ConfigViewModel::new() [lazy, cached]
|
||||
//! ├── Security → SecurityViewModel::new() [lazy, cached]
|
||||
//! └── About → AboutViewModel::new() [lazy, cached]
|
||||
//! ```
|
||||
//!
|
||||
//! ### State Management
|
||||
//!
|
||||
//! - **DeviceConnectionState**: Holds device status, FIDO info, LED config, errors
|
||||
//! - **AppModels**: Dependency injection bag holding `Entity<DeviceRepo>`
|
||||
//! - **DeviceRepo**: Reactive device state (status, FIDO info, LED config, errors)
|
||||
//! - **LayoutState**: Active view, sidebar width/collapse state
|
||||
//! - **ViewCache**: Cached entity views (PasskeysView, ConfigView) to preserve state
|
||||
//! - **ViewModelStore**: Lazy-initialized cached entity views, all use `get_or_insert_with`
|
||||
//!
|
||||
//! State flows downward from `ApplicationRoot` to views via props. Views communicate
|
||||
//! upward via callbacks (`on_select`, `on_refresh`) and events (`PasskeysEvent`).
|
||||
//! State flows via `Entity<T>::read(cx)`. Views subscribe to `DeviceEvent::Updated`
|
||||
//! and communicate upward via callbacks (`on_select`, `on_refresh`) and events (`PasskeysEvent`).
|
||||
//!
|
||||
//! ---
|
||||
//!
|
||||
|
||||
+224
-4
@@ -1,24 +1,39 @@
|
||||
//! Root application wiring — owns all shared state, layout, and view-model lifecycles.
|
||||
//!
|
||||
//! [`ApplicationRoot`] is the top-level GPUI component. It polls device hardware via
|
||||
//! `refresh_device_status`, routes between screens based on `ActiveView`, and manages
|
||||
//! the sidebar, title bar, and sidebar toggle button.
|
||||
|
||||
use crate::hal::io;
|
||||
use crate::hal::types::{DeviceMethod, FirmwareType};
|
||||
use crate::ui::components::sidebar::AppSidebar;
|
||||
use crate::ui::models::device::DeviceRepo;
|
||||
use crate::ui::screens::{
|
||||
about::AboutViewModel, config::ConfigView, home::HomeViewModel, passkeys::PasskeysView,
|
||||
security::SecurityViewModel,
|
||||
about::AboutViewModel, config::ConfigViewModel, home::HomeViewModel, passkeys::PasskeysEvent,
|
||||
passkeys::PasskeysViewModel, security::SecurityViewModel,
|
||||
};
|
||||
use gpui::prelude::*;
|
||||
use gpui::*;
|
||||
use gpui_component::Root;
|
||||
use gpui_component::{
|
||||
ActiveTheme, Icon, TitleBar, WindowExt, h_flex, scroll::ScrollableElement, v_flex,
|
||||
};
|
||||
|
||||
gpui::actions!(picoforge, [ToggleSidebar]);
|
||||
|
||||
/// Shared reactive models accessible to every screen view-model.
|
||||
pub struct AppModels {
|
||||
pub device: Entity<DeviceRepo>,
|
||||
}
|
||||
|
||||
/// Lazy-initialisation registry for screen view-models. Each field is `None`
|
||||
/// until its screen is navigated to, then created via `get_or_insert_with`.
|
||||
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>>,
|
||||
pub passkeys: Option<Entity<PasskeysViewModel>>,
|
||||
pub config: Option<Entity<ConfigViewModel>>,
|
||||
}
|
||||
|
||||
impl ViewModelStore {
|
||||
@@ -33,6 +48,7 @@ impl ViewModelStore {
|
||||
}
|
||||
}
|
||||
|
||||
/// Which screen is currently displayed in the content area.
|
||||
#[derive(Clone, Copy, PartialEq, Debug)]
|
||||
pub enum ActiveView {
|
||||
Home,
|
||||
@@ -42,6 +58,7 @@ pub enum ActiveView {
|
||||
About,
|
||||
}
|
||||
|
||||
/// Dimensions and collapse state of the sidebar panel.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct LayoutState {
|
||||
pub active_view: ActiveView,
|
||||
@@ -61,6 +78,7 @@ impl LayoutState {
|
||||
}
|
||||
}
|
||||
|
||||
/// Top-level GPUI component — owns models, layout, and wires the sidebar + content routing.
|
||||
pub struct ApplicationRoot {
|
||||
pub models: AppModels,
|
||||
pub view_state: LayoutState,
|
||||
@@ -69,6 +87,7 @@ pub struct ApplicationRoot {
|
||||
}
|
||||
|
||||
impl ApplicationRoot {
|
||||
/// Creates the root, initialises `DeviceRepo`, and triggers an immediate device poll.
|
||||
pub fn new(cx: &mut Context<Self>) -> Self {
|
||||
let device = cx.new(|_| DeviceRepo::new());
|
||||
|
||||
@@ -87,6 +106,9 @@ impl ApplicationRoot {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
|
||||
/// Polls the HAL layer for device status, FIDO info, LED config, and management app data.
|
||||
/// Writes results into [`DeviceRepo`] and refreshes subscribed view-models.
|
||||
/// Skips if a load is already in progress.
|
||||
pub(crate) fn refresh_device_status(
|
||||
&mut self,
|
||||
window: Option<&mut Window>,
|
||||
@@ -163,8 +185,206 @@ impl ApplicationRoot {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Toggles the sidebar between collapsed and expanded state.
|
||||
pub fn toggle_sidebar(&mut self, cx: &mut Context<Self>) {
|
||||
self.view_state.is_sidebar_collapsed = !self.view_state.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.view_state.is_sidebar_collapsed || !is_window_wide;
|
||||
|
||||
let target_width = if is_sidebar_collapsed {
|
||||
px(48.)
|
||||
} else {
|
||||
px(255.)
|
||||
};
|
||||
|
||||
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.view_state.sidebar_width = target_width;
|
||||
}
|
||||
|
||||
let dialog_layer = Root::render_dialog_layer(window, cx);
|
||||
let sheet_layer = Root::render_sheet_layer(window, cx);
|
||||
|
||||
let title_bar = TitleBar::new().bg(cx.theme().title_bar).child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.justify_between()
|
||||
.bg(cx.theme().title_bar)
|
||||
.items_center()
|
||||
.cursor(gpui::CursorStyle::OpenHand),
|
||||
);
|
||||
|
||||
let content_area = v_flex()
|
||||
.track_focus(&self.focus_handle)
|
||||
.key_context("ApplicationRoot")
|
||||
.on_action(cx.listener(|this, _: &ToggleSidebar, _, cx| {
|
||||
this.toggle_sidebar(cx);
|
||||
}))
|
||||
.min_h(px(0.))
|
||||
.min_w(px(0.))
|
||||
.overflow_y_scrollbar()
|
||||
.flex_grow()
|
||||
.bg(cx.theme().background)
|
||||
.child(match self.view_state.active_view {
|
||||
ActiveView::Home => {
|
||||
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_store.passkeys.get_or_insert_with(|| {
|
||||
let view = cx.new(|cx| PasskeysViewModel::new(window, cx, &self.models));
|
||||
cx.subscribe_in(
|
||||
&view,
|
||||
window,
|
||||
|_, _, event: &PasskeysEvent, window, cx| match event {
|
||||
PasskeysEvent::Notification(msg) => {
|
||||
window.push_notification(msg.to_string(), cx);
|
||||
}
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
view
|
||||
});
|
||||
view.clone().into_any_element()
|
||||
}
|
||||
ActiveView::Configuration => {
|
||||
let view = self.views_store.config.get_or_insert_with(|| {
|
||||
cx.new(|cx| ConfigViewModel::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()
|
||||
}
|
||||
});
|
||||
|
||||
let sidebar = AppSidebar::new(
|
||||
self.view_state.active_view,
|
||||
self.view_state.sidebar_width,
|
||||
is_sidebar_collapsed,
|
||||
&self.models,
|
||||
)
|
||||
.on_select(|this: &mut Self, view, _, _| {
|
||||
this.view_state.active_view = view;
|
||||
})
|
||||
.on_refresh(|this, window, cx| {
|
||||
this.refresh_device_status(Some(window), cx);
|
||||
});
|
||||
|
||||
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.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 {
|
||||
"icons/chevron-left.svg"
|
||||
};
|
||||
let toggle_tooltip = if is_sidebar_collapsed {
|
||||
"Expand"
|
||||
} else {
|
||||
"Collapse"
|
||||
};
|
||||
let toggle_btn = div()
|
||||
.id("sidebar-toggle-zone")
|
||||
.absolute()
|
||||
.left(sidebar_width - px(14.))
|
||||
.top_0()
|
||||
.bottom_0()
|
||||
.w(px(28.))
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.on_hover(cx.listener(|this, hovered, _, cx| {
|
||||
this.view_state.sidebar_toggle_hovered = *hovered;
|
||||
cx.notify();
|
||||
}))
|
||||
.child(
|
||||
div()
|
||||
.id("sidebar-toggle-btn")
|
||||
.w(px(24.))
|
||||
.h(px(24.))
|
||||
.rounded_full()
|
||||
.bg(sidebar_bg)
|
||||
.border_1()
|
||||
.border_color(border_color)
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.cursor(gpui::CursorStyle::PointingHand)
|
||||
.opacity(if is_toggle_visible { 1.0 } else { 0.0 })
|
||||
.tooltip(move |window, cx| {
|
||||
gpui_component::tooltip::Tooltip::new(toggle_tooltip)
|
||||
.action(&ToggleSidebar, None)
|
||||
.build(window, cx)
|
||||
})
|
||||
.on_click(cx.listener(|this, _, _, _| {
|
||||
this.view_state.is_sidebar_collapsed =
|
||||
!this.view_state.is_sidebar_collapsed;
|
||||
}))
|
||||
.child(Icon::default().path(toggle_icon).text_color(sidebar_fg)),
|
||||
);
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
let content_column = content_area;
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let content_column = v_flex().size_full().child(title_bar).child(content_area);
|
||||
|
||||
let main_area = h_flex()
|
||||
.id("main-area")
|
||||
.relative()
|
||||
.items_start()
|
||||
.map(|this| {
|
||||
if cfg!(target_os = "macos") {
|
||||
this.flex_1().min_h(px(0.))
|
||||
} else {
|
||||
this.size_full()
|
||||
}
|
||||
})
|
||||
.child(
|
||||
div()
|
||||
.h_full()
|
||||
.w(sidebar_width)
|
||||
.flex_shrink_0()
|
||||
.child(sidebar.render(cx)),
|
||||
)
|
||||
.child(content_column.h_full().flex_1().w_0())
|
||||
.child(toggle_btn);
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
let body = v_flex().size_full().child(title_bar).child(main_area);
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let body = main_area;
|
||||
|
||||
div()
|
||||
.id("application-root")
|
||||
.size_full()
|
||||
.overflow_hidden()
|
||||
.child(body)
|
||||
.children(dialog_layer)
|
||||
.children(sheet_layer)
|
||||
}
|
||||
}
|
||||
|
||||
+136
-1
@@ -1,7 +1,142 @@
|
||||
//! # PicoForge UI Layer
|
||||
//!
|
||||
//! This module is the entire frontend of PicoForge — a GPUI-based desktop application
|
||||
//! that communicates with physical Pico FIDO / RS-Key hardware security keys.
|
||||
//!
|
||||
//! ## Architecture
|
||||
//!
|
||||
//! The UI follows a **reactive component tree** model. At the root sits
|
||||
//! [`ApplicationRoot`](app::ApplicationRoot), which holds:
|
||||
//!
|
||||
//! * **Shared reactive state** — [`AppModels`](app::AppModels) wrapping
|
||||
//! [`DeviceRepo`](models::device::DeviceRepo), an `Entity<DeviceRepo>` that any
|
||||
//! view-model can read or write.
|
||||
//! * **View-model registry** — [`ViewModelStore`](app::ViewModelStore) that
|
||||
//! lazily initializes each screen's view-model on first navigation.
|
||||
//! * **Layout state** — [`LayoutState`](app::LayoutState) tracking active sidebar
|
||||
//! selection and sidebar width/collapse.
|
||||
//!
|
||||
//! Each screen (Home, Passkeys, Configuration, Security, About) is split into:
|
||||
//! * `view_model.rs` — the reactive state machine (`Entity<T>`, `EventEmitter<T>`)
|
||||
//! * `view.rs` — the `Render` impl that builds GPUI elements from view-model state
|
||||
//!
|
||||
//! Views never call device hardware directly; they read from `DeviceRepo` (which
|
||||
//! is populated by `ApplicationRoot::refresh_device_status`) and emit events that
|
||||
//! flow upward through the component tree.
|
||||
//!
|
||||
//! ## GPUI Concepts (zed-industries/gpui)
|
||||
//!
|
||||
//! GPUI — <https://github.com/zed-industries/zed/tree/main/crates/gpui> — is a
|
||||
//! hybrid immediate/retained-mode GPU-accelerated UI framework for Rust.
|
||||
//!
|
||||
//! | Concept | Role |
|
||||
//! |---|---|
|
||||
//! | `Entity<T>` | Reference-counted smart pointer to reactive state. Created via `cx.new(\|cx\| T::new(...))`. |
|
||||
//! | `Context<Self>` | (also `WindowContext`) — used for creating entities, subscribing to events, notifying. |
|
||||
//! | `Window` | Window-level operations: animations, notifications, dialogs, bounds. |
|
||||
//! | `Render` trait | `fn render(&mut self, window, cx) -> impl IntoElement`. The framework calls this on `cx.notify()`. |
|
||||
//! | `cx.notify()` | Schedule a re-render. Every mutation to reactive state (inside `Entity::update`) must call this. |
|
||||
//! | `cx.subscribe(&entity, callback)` | Listen for events emitted by another entity. **Unbounded** — must `cx.subscribe_in` and `detach()` or leak. |
|
||||
//! | `EventEmitter<E>` | Trait an entity impls to declare it emits events of type `E`. |
|
||||
//! | `FocusHandle` | Focus management — returned by `cx.focus_handle()`. Needed for keyboard event routing (`track_focus`, `key_context`). |
|
||||
//! | `actions!` | Macro for defining custom actions (e.g. `ToggleSidebar`). Bound to elements via `on_action(...)`. |
|
||||
//!
|
||||
//! **Lifecycle:** When state changes, call `cx.notify()` inside an `Entity::update`
|
||||
//! closure. GPUI re-invokes `Render` on the next frame, comparing the new element
|
||||
//! tree against the previous one (reconciliation).
|
||||
//!
|
||||
//! ## gpui-component Concepts (longbridge/gpui-component)
|
||||
//!
|
||||
//! gpui-component — <https://github.com/longbridge/gpui-component> — provides
|
||||
//! 60+ pre-built widgets on top of GPUI. PicoForge uses the **librekeys fork**
|
||||
//! (`fix/client-window-linux` branch).
|
||||
//!
|
||||
//! * **Stateless widgets** — builder-pattern constructors like
|
||||
//! `Button::new("id").primary().label("text").on_click(...)`.
|
||||
//! These are lightweight; they produce `impl IntoElement` in `Render`.
|
||||
//! * **Stateful components** — `InputState`, `SelectState`, `SliderState`, etc.,
|
||||
//! created via `cx.new(|cx| ...)` and stored in the view-model. They need
|
||||
//! `cx.subscribe(&state, callback)` to react to user interaction.
|
||||
//! * **Theming** — `ActiveTheme` trait on `cx` (e.g. `cx.theme().background`).
|
||||
//! Themes come from JSON (e.g. `themes/picoforge-zinc.json`). `Root::render_dialog_layer`
|
||||
//! and `Root::render_sheet_layer` must be called in the root `Render`.
|
||||
//! * **Layout helpers** — `h_flex`, `v_flex` for flexbox-style layouts,
|
||||
//! `scroll::ScrollableElement` for scroll containers, `TitleBar` for window chrome,
|
||||
//! `Icon` for SVG icons, `tooltip::Tooltip` for hover tooltips.
|
||||
//! * **`Root`** — top-level wrapper created once per window:
|
||||
//! `cx.new(|cx| Root::new(content_view, window, cx))`.
|
||||
//!
|
||||
//! ## Tree Structure (file-by-file)
|
||||
//!
|
||||
//! ```text
|
||||
//! src/ui/
|
||||
//! ├── mod.rs # This file — module declaration + architecture docs
|
||||
//! ├── app.rs # ApplicationRoot, AppModels, LayoutState, ViewModelStore
|
||||
//! │ # Root Render: sidebar, title bar, content routing, toggle button
|
||||
//! │ # refresh_device_status: polls HAL, updates DeviceRepo
|
||||
//! ├── assets.rs # AssetLoaderImpl via rust-embed (loads SVGs from static/)
|
||||
//! ├── colors.rs # Zinc palette constants (u32 RGB). WIP — HSLA migration planned.
|
||||
//! │ # Reference: https://ui.shadcn.com/colors
|
||||
//! ├── models/
|
||||
//! │ ├── mod.rs # pub mod device
|
||||
//! │ └── device.rs # DeviceRepo — reactive state for device status, FIDO info,
|
||||
//! │ # LED config, management apps, loading/error flags.
|
||||
//! │ # Implements EventEmitter<DeviceEvent>
|
||||
//! ├── components/
|
||||
//! │ ├── mod.rs # Module declarations for sub-components
|
||||
//! │ ├── button.rs # Custom button widgets
|
||||
//! │ ├── card.rs # Card container widgets
|
||||
//! │ ├── dialog.rs # Custom dialog widgets
|
||||
//! │ ├── page_view.rs # Page view layout container
|
||||
//! │ ├── sidebar.rs # AppSidebar — SVG navigation sidebar (Home, Passkeys, etc.)
|
||||
//! │ │ # Handles selection highlighting, tooltips, nav callbacks
|
||||
//! │ └── tag.rs # Tag/badge widgets
|
||||
//! ├── screens/
|
||||
//! │ ├── mod.rs # pub mod home, config, passkeys, security, about
|
||||
//! │ ├── home/
|
||||
//! │ │ ├── mod.rs # HomeView re-export
|
||||
//! │ │ ├── view_model.rs # HomeViewModel — device summary state
|
||||
//! │ │ └── view.rs # HomeView — device status cards, connection info
|
||||
//! │ ├── config/
|
||||
//! │ │ ├── mod.rs # ConfigView re-export
|
||||
//! │ │ ├── view_model.rs # ConfigViewModel — PIN management, LED, transport config
|
||||
//! │ │ └── view.rs # ConfigView — configuration form UI
|
||||
//! │ ├── passkeys/
|
||||
//! │ │ ├── mod.rs # PasskeysView re-export
|
||||
//! │ │ ├── view_model.rs # PasskeysViewModel — credential list, unlock state
|
||||
//! │ │ └── view.rs # PasskeysView — passkey table, credential operations
|
||||
//! │ ├── security/
|
||||
//! │ │ ├── mod.rs # SecurityView re-export
|
||||
//! │ │ ├── view_model.rs # SecurityViewModel — reset, attestation, FIDO2 config
|
||||
//! │ │ └── view.rs # SecurityView — security settings UI
|
||||
//! │ └── about/
|
||||
//! │ ├── mod.rs # AboutView re-export
|
||||
//! │ ├── view_model.rs # AboutViewModel — version, firmware details
|
||||
//! │ └── view.rs # AboutView — build info, licenses, firmware version
|
||||
//! ```
|
||||
//!
|
||||
//! ## Navigation & Data Flow
|
||||
//!
|
||||
//! 1. `ApplicationRoot::render` reads `LayoutState.active_view` to decide which
|
||||
//! screen to display. Each screen's view-model is lazily created via
|
||||
//! `get_or_insert_with` on `ViewModelStore`.
|
||||
//! 2. `AppSidebar::on_select` sets `LayoutState.active_view` and calls `cx.notify()`.
|
||||
//! 3. `refresh_device_status` (called at startup and on sidebar refresh) invokes
|
||||
//! `crate::hal::io::read_device_details()`, writes results into `DeviceRepo`,
|
||||
//! then notifies all subscribers.
|
||||
//! 4. Screen view-models read `DeviceRepo` in their `Render` or event handlers
|
||||
//! via `models.device.read(cx)`.
|
||||
//!
|
||||
//! ## External References
|
||||
//!
|
||||
//! * GPUI framework: <https://github.com/zed-industries/zed/tree/main/crates/gpui>
|
||||
//! * gpui-component library: <https://github.com/longbridge/gpui-component>
|
||||
//! * gpui-component docs: <https://longbridge.github.io/gpui-component/>
|
||||
//! * shadcn color palette (used in `colors.rs`): <https://ui.shadcn.com/colors>
|
||||
|
||||
pub mod app;
|
||||
pub mod assets;
|
||||
pub mod colors;
|
||||
pub mod components;
|
||||
pub mod models;
|
||||
pub mod rootview;
|
||||
pub mod screens;
|
||||
|
||||
@@ -1,209 +0,0 @@
|
||||
use crate::ui::app::{ActiveView, ApplicationRoot, ToggleSidebar};
|
||||
use crate::ui::components::sidebar::AppSidebar;
|
||||
use crate::ui::screens::{
|
||||
about::AboutViewModel, config::ConfigView, home::HomeViewModel, passkeys::PasskeysEvent,
|
||||
passkeys::PasskeysView, security::SecurityViewModel,
|
||||
};
|
||||
use gpui::prelude::*;
|
||||
use gpui::*;
|
||||
use gpui_component::Root;
|
||||
use gpui_component::{
|
||||
ActiveTheme, Icon, TitleBar, WindowExt, h_flex, scroll::ScrollableElement, v_flex,
|
||||
};
|
||||
|
||||
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.view_state.is_sidebar_collapsed || !is_window_wide;
|
||||
|
||||
let target_width = if is_sidebar_collapsed {
|
||||
px(48.)
|
||||
} else {
|
||||
px(255.)
|
||||
};
|
||||
|
||||
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.view_state.sidebar_width = target_width;
|
||||
}
|
||||
|
||||
let dialog_layer = Root::render_dialog_layer(window, cx);
|
||||
let sheet_layer = Root::render_sheet_layer(window, cx);
|
||||
|
||||
let title_bar = TitleBar::new().bg(cx.theme().title_bar).child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.justify_between()
|
||||
.bg(cx.theme().title_bar)
|
||||
.items_center()
|
||||
.cursor(gpui::CursorStyle::OpenHand),
|
||||
);
|
||||
|
||||
let content_area = v_flex()
|
||||
.track_focus(&self.focus_handle)
|
||||
.key_context("ApplicationRoot")
|
||||
.on_action(cx.listener(|this, _: &ToggleSidebar, _, cx| {
|
||||
this.toggle_sidebar(cx);
|
||||
}))
|
||||
.min_h(px(0.))
|
||||
.min_w(px(0.))
|
||||
.overflow_y_scrollbar()
|
||||
.flex_grow()
|
||||
.bg(cx.theme().background)
|
||||
.child(match self.view_state.active_view {
|
||||
ActiveView::Home => {
|
||||
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_store.passkeys.get_or_insert_with(|| {
|
||||
let view = cx.new(|cx| PasskeysView::new(window, cx, &self.models));
|
||||
cx.subscribe_in(
|
||||
&view,
|
||||
window,
|
||||
|_, _, event: &PasskeysEvent, window, cx| match event {
|
||||
PasskeysEvent::Notification(msg) => {
|
||||
window.push_notification(msg.to_string(), cx);
|
||||
}
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
view
|
||||
});
|
||||
view.clone().into_any_element()
|
||||
}
|
||||
ActiveView::Configuration => {
|
||||
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()
|
||||
}
|
||||
});
|
||||
|
||||
let sidebar = AppSidebar::new(
|
||||
self.view_state.active_view,
|
||||
self.view_state.sidebar_width,
|
||||
is_sidebar_collapsed,
|
||||
&self.models,
|
||||
)
|
||||
.on_select(|this: &mut Self, view, _, _| {
|
||||
this.view_state.active_view = view;
|
||||
})
|
||||
.on_refresh(|this, window, cx| {
|
||||
this.refresh_device_status(Some(window), cx);
|
||||
});
|
||||
|
||||
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.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 {
|
||||
"icons/chevron-left.svg"
|
||||
};
|
||||
let toggle_tooltip = if is_sidebar_collapsed {
|
||||
"Expand"
|
||||
} else {
|
||||
"Collapse"
|
||||
};
|
||||
let toggle_btn = div()
|
||||
.id("sidebar-toggle-zone")
|
||||
.absolute()
|
||||
.left(sidebar_width - px(14.))
|
||||
.top_0()
|
||||
.bottom_0()
|
||||
.w(px(28.))
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.on_hover(cx.listener(|this, hovered, _, cx| {
|
||||
this.view_state.sidebar_toggle_hovered = *hovered;
|
||||
cx.notify();
|
||||
}))
|
||||
.child(
|
||||
div()
|
||||
.id("sidebar-toggle-btn")
|
||||
.w(px(24.))
|
||||
.h(px(24.))
|
||||
.rounded_full()
|
||||
.bg(sidebar_bg)
|
||||
.border_1()
|
||||
.border_color(border_color)
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.cursor(gpui::CursorStyle::PointingHand)
|
||||
.opacity(if is_toggle_visible { 1.0 } else { 0.0 })
|
||||
.tooltip(move |window, cx| {
|
||||
gpui_component::tooltip::Tooltip::new(toggle_tooltip)
|
||||
.action(&ToggleSidebar, None)
|
||||
.build(window, cx)
|
||||
})
|
||||
.on_click(cx.listener(|this, _, _, _| {
|
||||
this.view_state.is_sidebar_collapsed =
|
||||
!this.view_state.is_sidebar_collapsed;
|
||||
}))
|
||||
.child(Icon::default().path(toggle_icon).text_color(sidebar_fg)),
|
||||
);
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
let content_column = content_area;
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let content_column = v_flex().size_full().child(title_bar).child(content_area);
|
||||
|
||||
let main_area = h_flex()
|
||||
.id("main-area")
|
||||
.relative()
|
||||
.items_start()
|
||||
.map(|this| {
|
||||
if cfg!(target_os = "macos") {
|
||||
this.flex_1().min_h(px(0.))
|
||||
} else {
|
||||
this.size_full()
|
||||
}
|
||||
})
|
||||
.child(
|
||||
div()
|
||||
.h_full()
|
||||
.w(sidebar_width)
|
||||
.flex_shrink_0()
|
||||
.child(sidebar.render(cx)),
|
||||
)
|
||||
.child(content_column.h_full().flex_1().w_0())
|
||||
.child(toggle_btn);
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
let body = v_flex().size_full().child(title_bar).child(main_area);
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let body = main_area;
|
||||
|
||||
div()
|
||||
.id("application-root")
|
||||
.size_full()
|
||||
.overflow_hidden()
|
||||
.child(body)
|
||||
.children(dialog_layer)
|
||||
.children(sheet_layer)
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
pub mod view;
|
||||
pub mod view_model;
|
||||
pub use view_model::ConfigView;
|
||||
pub use view_model::ConfigViewModel;
|
||||
|
||||
@@ -4,7 +4,7 @@ use crate::hal::rescue::constants::{
|
||||
};
|
||||
use crate::hal::types::DeviceMethod;
|
||||
use crate::ui::components::{card::Card, page_view::PageView};
|
||||
use crate::ui::screens::config::view_model::ConfigView;
|
||||
use crate::ui::screens::config::view_model::ConfigViewModel;
|
||||
use gpui::*;
|
||||
use gpui_component::button::{ButtonCustomVariant, ButtonVariants};
|
||||
use gpui_component::{
|
||||
@@ -12,7 +12,7 @@ use gpui_component::{
|
||||
slider::Slider, switch::Switch, v_flex,
|
||||
};
|
||||
|
||||
impl ConfigView {
|
||||
impl ConfigViewModel {
|
||||
fn render_identity_card(
|
||||
&self,
|
||||
theme: &Theme,
|
||||
@@ -549,7 +549,7 @@ impl ConfigView {
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ConfigView {
|
||||
impl Render for ConfigViewModel {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let theme = cx.theme();
|
||||
let has_device = self.device.read(cx).status.is_some();
|
||||
@@ -581,7 +581,7 @@ impl Render for ConfigView {
|
||||
let is_fido = status.as_ref().map(|s| s.method.clone()) == Some(DeviceMethod::Fido);
|
||||
let supports_legacy_fido_config = status
|
||||
.as_ref()
|
||||
.map(ConfigView::status_supports_legacy_fido_config)
|
||||
.map(ConfigViewModel::status_supports_legacy_fido_config)
|
||||
.unwrap_or(false);
|
||||
let hardware_config_disabled = is_fido && !supports_legacy_fido_config;
|
||||
|
||||
|
||||
@@ -193,7 +193,7 @@ pub(super) enum StatusDialogHandle {
|
||||
Status(WeakEntity<StatusContent>),
|
||||
}
|
||||
|
||||
pub struct ConfigView {
|
||||
pub struct ConfigViewModel {
|
||||
pub(super) device: Entity<DeviceRepo>,
|
||||
pub(super) vendor_select: Entity<SelectState<Vec<VendorSelectOption>>>,
|
||||
pub(super) vid_input: Entity<InputState>,
|
||||
@@ -221,7 +221,7 @@ pub struct ConfigView {
|
||||
pub(super) _task: Option<Task<()>>,
|
||||
}
|
||||
|
||||
impl ConfigView {
|
||||
impl ConfigViewModel {
|
||||
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())
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
pub mod view;
|
||||
pub mod view_model;
|
||||
pub use view_model::{PasskeysEvent, PasskeysView};
|
||||
pub use view_model::{PasskeysEvent, PasskeysViewModel};
|
||||
|
||||
@@ -5,7 +5,7 @@ use crate::ui::components::{
|
||||
dialog,
|
||||
page_view::PageView,
|
||||
};
|
||||
use crate::ui::screens::passkeys::view_model::{PasskeysEvent, PasskeysView};
|
||||
use crate::ui::screens::passkeys::view_model::{PasskeysEvent, PasskeysViewModel};
|
||||
use directories::UserDirs;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::*;
|
||||
@@ -15,7 +15,7 @@ use gpui_component::{
|
||||
ActiveTheme, Icon, Sizable, StyledExt, Theme, badge::Badge, h_flex, switch::Switch, v_flex,
|
||||
};
|
||||
|
||||
impl PasskeysView {
|
||||
impl PasskeysViewModel {
|
||||
fn render_enterprise_attestation(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let csr_ready = self.csr_pem.is_some();
|
||||
let show_csr = self.show_csr && csr_ready;
|
||||
@@ -686,7 +686,7 @@ impl PasskeysView {
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for PasskeysView {
|
||||
impl Render for PasskeysViewModel {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let device = self.device.read(cx);
|
||||
let device_connected = device.status.is_some();
|
||||
|
||||
@@ -10,7 +10,7 @@ use gpui::*;
|
||||
use gpui_component::button::ButtonVariants;
|
||||
use gpui_component::{ActiveTheme, StyledExt, WindowExt};
|
||||
|
||||
pub struct PasskeysView {
|
||||
pub struct PasskeysViewModel {
|
||||
pub(super) device: Entity<DeviceRepo>,
|
||||
pub(super) credentials: Vec<StoredCredential>,
|
||||
pub(super) unlocked: bool,
|
||||
@@ -26,9 +26,9 @@ pub enum PasskeysEvent {
|
||||
Notification(String),
|
||||
}
|
||||
|
||||
impl EventEmitter<PasskeysEvent> for PasskeysView {}
|
||||
impl EventEmitter<PasskeysEvent> for PasskeysViewModel {}
|
||||
|
||||
impl PasskeysView {
|
||||
impl PasskeysViewModel {
|
||||
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())
|
||||
|
||||
Reference in New Issue
Block a user