2026-01-29 17:42:03 +05:30
|
|
|
use crate::device::io;
|
|
|
|
|
use crate::ui::components::sidebar::AppSidebar;
|
2026-03-05 23:19:07 +05:30
|
|
|
use crate::ui::types::{ActiveView, DeviceConnectionState, LayoutState, ViewCache};
|
2026-02-22 00:28:30 +05:30
|
|
|
use crate::ui::views::{
|
2026-03-04 18:11:19 +05:30
|
|
|
about::AboutView, config::ConfigView, home::HomeView, passkeys::PasskeysEvent,
|
2026-02-22 00:28:30 +05:30
|
|
|
passkeys::PasskeysView, security::SecurityView,
|
2026-01-27 22:51:03 +05:30
|
|
|
};
|
2026-01-29 22:56:33 +05:30
|
|
|
use gpui::prelude::*;
|
2026-01-27 22:51:03 +05:30
|
|
|
use gpui::*;
|
2026-02-05 18:10:43 +05:30
|
|
|
use gpui_component::Root;
|
2026-01-28 23:36:15 +05:30
|
|
|
use gpui_component::{
|
2026-02-24 15:01:51 +01:00
|
|
|
ActiveTheme, Icon, TitleBar, WindowExt, h_flex, scroll::ScrollableElement, v_flex,
|
2026-01-28 23:36:15 +05:30
|
|
|
};
|
2026-01-27 22:51:03 +05:30
|
|
|
|
2026-02-24 15:01:51 +01:00
|
|
|
gpui::actions!(picoforge, [ToggleSidebar]);
|
|
|
|
|
|
2026-01-27 22:51:03 +05:30
|
|
|
pub struct ApplicationRoot {
|
2026-03-05 23:19:07 +05:30
|
|
|
pub device: DeviceConnectionState,
|
|
|
|
|
pub layout: LayoutState,
|
|
|
|
|
pub views: ViewCache,
|
|
|
|
|
pub focus_handle: FocusHandle,
|
2026-01-27 22:51:03 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl ApplicationRoot {
|
2026-01-28 23:36:15 +05:30
|
|
|
pub fn new(cx: &mut Context<Self>) -> Self {
|
|
|
|
|
let mut this = Self {
|
2026-03-05 23:19:07 +05:30
|
|
|
device: DeviceConnectionState::new(),
|
|
|
|
|
layout: LayoutState::new(),
|
|
|
|
|
views: ViewCache::new(),
|
2026-02-24 15:01:51 +01:00
|
|
|
focus_handle: cx.focus_handle(),
|
2026-01-28 23:36:15 +05:30
|
|
|
};
|
2026-03-05 23:19:07 +05:30
|
|
|
|
2026-02-08 18:05:18 +05:30
|
|
|
this.refresh_device_status(None, cx);
|
2026-01-28 23:36:15 +05:30
|
|
|
this
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-24 15:01:51 +01:00
|
|
|
pub fn focus_handle(&self) -> FocusHandle {
|
|
|
|
|
self.focus_handle.clone()
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-08 18:05:18 +05:30
|
|
|
fn refresh_device_status(&mut self, window: Option<&mut Window>, cx: &mut Context<Self>) {
|
2026-03-05 23:19:07 +05:30
|
|
|
if self.device.loading {
|
2026-01-28 23:36:15 +05:30
|
|
|
return;
|
2026-01-28 17:29:07 +05:30
|
|
|
}
|
2026-03-05 23:19:07 +05:30
|
|
|
self.device.loading = true;
|
|
|
|
|
self.device.error = None;
|
2026-01-28 23:36:15 +05:30
|
|
|
cx.notify();
|
|
|
|
|
|
2026-01-29 17:42:03 +05:30
|
|
|
match io::read_device_details() {
|
|
|
|
|
Ok(status) => {
|
2026-03-06 00:38:45 +05:30
|
|
|
let device_changed = self
|
|
|
|
|
.device
|
|
|
|
|
.status
|
|
|
|
|
.as_ref()
|
|
|
|
|
.map(|s| s.info.serial != status.info.serial)
|
|
|
|
|
.unwrap_or(true);
|
|
|
|
|
|
|
|
|
|
self.device.status = Some(status);
|
2026-03-05 23:19:07 +05:30
|
|
|
self.device.error = None;
|
2026-01-29 17:42:03 +05:30
|
|
|
|
2026-03-06 00:38:45 +05:30
|
|
|
if device_changed {
|
|
|
|
|
self.views.passkeys = None;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-29 17:42:03 +05:30
|
|
|
match io::get_fido_info() {
|
|
|
|
|
Ok(fido) => {
|
2026-03-05 23:19:07 +05:30
|
|
|
self.device.fido_info = Some(fido);
|
2026-01-29 17:42:03 +05:30
|
|
|
}
|
|
|
|
|
Err(e) => {
|
2026-02-20 22:29:39 +05:30
|
|
|
log::error!("FIDO Info fetch failed: {}", e);
|
2026-03-05 23:19:07 +05:30
|
|
|
self.device.fido_info = None;
|
2026-01-29 17:42:03 +05:30
|
|
|
}
|
|
|
|
|
}
|
2026-02-03 21:31:29 +05:30
|
|
|
|
2026-03-05 23:19:07 +05:30
|
|
|
if let Some(config_view) = &self.views.config
|
2026-03-04 18:11:19 +05:30
|
|
|
&& let Some(window) = window
|
|
|
|
|
{
|
2026-03-06 00:38:45 +05:30
|
|
|
let device = self.device.clone();
|
2026-03-04 18:11:19 +05:30
|
|
|
config_view.update(cx, |view, cx| {
|
2026-03-06 00:38:45 +05:30
|
|
|
view.sync_from_device(&device, window, cx);
|
2026-02-03 21:31:29 +05:30
|
|
|
});
|
|
|
|
|
}
|
2026-01-29 17:42:03 +05:30
|
|
|
}
|
|
|
|
|
Err(e) => {
|
2026-03-05 23:19:07 +05:30
|
|
|
self.device.status = None;
|
|
|
|
|
self.device.error = Some(format!("{}", e));
|
|
|
|
|
self.device.fido_info = None;
|
2026-01-29 17:42:03 +05:30
|
|
|
}
|
|
|
|
|
}
|
2026-03-05 23:19:07 +05:30
|
|
|
self.device.loading = false;
|
2026-01-29 17:42:03 +05:30
|
|
|
cx.notify();
|
2026-01-28 17:29:07 +05:30
|
|
|
}
|
2026-02-24 15:01:51 +01:00
|
|
|
|
|
|
|
|
pub fn toggle_sidebar(&mut self, cx: &mut Context<Self>) {
|
2026-03-05 23:19:07 +05:30
|
|
|
self.layout.is_sidebar_collapsed = !self.layout.is_sidebar_collapsed;
|
2026-02-24 15:01:51 +01:00
|
|
|
cx.notify();
|
|
|
|
|
}
|
2026-01-27 22:51:03 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Render for ApplicationRoot {
|
2026-01-29 00:14:37 +05:30
|
|
|
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
2026-02-18 16:40:51 +05:30
|
|
|
let window_width = window.bounds().size.width;
|
|
|
|
|
let is_window_wide = window_width > px(800.0);
|
2026-03-05 23:19:07 +05:30
|
|
|
let is_sidebar_collapsed = self.layout.is_sidebar_collapsed || !is_window_wide;
|
2026-02-18 16:40:51 +05:30
|
|
|
|
|
|
|
|
let target_width = if is_sidebar_collapsed {
|
|
|
|
|
px(48.)
|
|
|
|
|
} else {
|
|
|
|
|
px(255.)
|
|
|
|
|
};
|
2026-02-17 23:03:08 +05:30
|
|
|
|
2026-03-05 23:19:07 +05:30
|
|
|
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;
|
2026-01-29 00:14:37 +05:30
|
|
|
window.request_animation_frame();
|
|
|
|
|
} else {
|
2026-03-05 23:19:07 +05:30
|
|
|
self.layout.sidebar_width = target_width;
|
2026-01-29 00:14:37 +05:30
|
|
|
}
|
|
|
|
|
|
2026-03-04 18:11:19 +05:30
|
|
|
let dialog_layer = Root::render_dialog_layer(window, cx);
|
|
|
|
|
let sheet_layer = Root::render_sheet_layer(window, cx);
|
2026-02-05 18:10:43 +05:30
|
|
|
|
2026-02-22 10:38:22 +01:00
|
|
|
let title_bar = TitleBar::new().bg(cx.theme().title_bar).child(
|
|
|
|
|
h_flex()
|
|
|
|
|
.w_full()
|
|
|
|
|
.justify_between()
|
|
|
|
|
.bg(cx.theme().title_bar)
|
|
|
|
|
.items_center()
|
2026-02-24 15:01:51 +01:00
|
|
|
.cursor(gpui::CursorStyle::OpenHand),
|
2026-02-22 10:38:22 +01:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let content_area = v_flex()
|
2026-03-01 00:29:27 +05:30
|
|
|
.track_focus(&self.focus_handle)
|
|
|
|
|
.key_context("ApplicationRoot")
|
|
|
|
|
.on_action(cx.listener(|this, _: &ToggleSidebar, _, cx| {
|
|
|
|
|
this.toggle_sidebar(cx);
|
|
|
|
|
}))
|
2026-02-22 10:38:22 +01:00
|
|
|
.min_h(px(0.))
|
|
|
|
|
.min_w(px(0.))
|
|
|
|
|
.overflow_y_scrollbar()
|
|
|
|
|
.flex_grow()
|
|
|
|
|
.bg(cx.theme().background)
|
2026-03-05 23:19:07 +05:30
|
|
|
.child(match self.layout.active_view {
|
2026-02-22 10:38:22 +01:00
|
|
|
ActiveView::Home => {
|
2026-03-05 23:19:07 +05:30
|
|
|
HomeView::build(&self.device, cx.theme(), window.bounds().size.width)
|
2026-02-22 10:38:22 +01:00
|
|
|
.into_any_element()
|
|
|
|
|
}
|
|
|
|
|
ActiveView::Passkeys => {
|
2026-03-05 23:19:07 +05:30
|
|
|
let view = self.views.passkeys.get_or_insert_with(|| {
|
2026-03-06 00:38:45 +05:30
|
|
|
let root = cx.entity().downgrade();
|
|
|
|
|
let view = cx.new(|cx| PasskeysView::new(window, cx, root));
|
2026-02-22 10:38:22 +01:00
|
|
|
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 => {
|
2026-03-06 00:38:45 +05:30
|
|
|
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()
|
2026-02-22 10:38:22 +01:00
|
|
|
}
|
|
|
|
|
ActiveView::Security => SecurityView::build(cx).into_any_element(),
|
|
|
|
|
ActiveView::About => AboutView::build(cx.theme()).into_any_element(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let sidebar = AppSidebar::new(
|
2026-03-05 23:19:07 +05:30
|
|
|
self.layout.active_view,
|
|
|
|
|
self.layout.sidebar_width,
|
2026-02-22 10:38:22 +01:00
|
|
|
is_sidebar_collapsed,
|
2026-03-05 23:19:07 +05:30
|
|
|
self.device.clone(),
|
2026-02-22 10:38:22 +01:00
|
|
|
)
|
|
|
|
|
.on_select(|this: &mut Self, view, _, _| {
|
2026-03-05 23:19:07 +05:30
|
|
|
this.layout.active_view = view;
|
2026-02-22 10:38:22 +01:00
|
|
|
})
|
|
|
|
|
.on_refresh(|this, window, cx| {
|
|
|
|
|
this.refresh_device_status(Some(window), cx);
|
|
|
|
|
});
|
|
|
|
|
|
2026-02-24 15:01:51 +01:00
|
|
|
// 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;
|
2026-03-05 23:19:07 +05:30
|
|
|
let is_toggle_visible = !is_sidebar_collapsed || self.layout.sidebar_toggle_hovered;
|
|
|
|
|
let sidebar_width = self.layout.sidebar_width;
|
2026-02-24 15:01:51 +01:00
|
|
|
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| {
|
2026-03-05 23:19:07 +05:30
|
|
|
this.layout.sidebar_toggle_hovered = *hovered;
|
2026-02-24 15:01:51 +01:00
|
|
|
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, _, _, _| {
|
2026-03-05 23:19:07 +05:30
|
|
|
this.layout.is_sidebar_collapsed = !this.layout.is_sidebar_collapsed;
|
2026-02-24 15:01:51 +01:00
|
|
|
}))
|
|
|
|
|
.child(Icon::default().path(toggle_icon).text_color(sidebar_fg)),
|
|
|
|
|
);
|
|
|
|
|
|
2026-02-22 10:38:22 +01:00
|
|
|
#[cfg(target_os = "macos")]
|
2026-02-24 15:01:51 +01:00
|
|
|
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);
|
2026-02-21 22:24:16 +01:00
|
|
|
|
2026-02-22 10:38:22 +01:00
|
|
|
#[cfg(not(target_os = "macos"))]
|
2026-02-24 15:01:51 +01:00
|
|
|
let body = main_area;
|
2026-02-21 22:24:16 +01:00
|
|
|
|
2026-02-22 10:38:22 +01:00
|
|
|
div()
|
2026-02-24 15:01:51 +01:00
|
|
|
.id("application-root")
|
2026-02-22 10:38:22 +01:00
|
|
|
.size_full()
|
|
|
|
|
.overflow_hidden()
|
|
|
|
|
.child(body)
|
|
|
|
|
.children(dialog_layer)
|
2026-02-22 15:51:55 +05:30
|
|
|
.children(sheet_layer)
|
2026-01-28 17:29:07 +05:30
|
|
|
}
|
2026-01-27 22:51:03 +05:30
|
|
|
}
|