Files
picoforge/src/ui/rootview.rs
T

289 lines
10 KiB
Rust
Raw Normal View History

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};
use crate::ui::views::{
2026-03-04 18:11:19 +05:30
about::AboutView, config::ConfigView, home::HomeView, passkeys::PasskeysEvent,
passkeys::PasskeysView, security::SecurityView,
};
use gpui::prelude::*;
use gpui::*;
use gpui_component::Root;
use gpui_component::{
2026-02-24 15:01:51 +01:00
ActiveTheme, Icon, TitleBar, WindowExt, h_flex, scroll::ScrollableElement, v_flex,
};
2026-02-24 15:01:51 +01:00
gpui::actions!(picoforge, [ToggleSidebar]);
pub struct ApplicationRoot {
2026-03-05 23:19:07 +05:30
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 {
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-03-05 23:19:07 +05:30
this.refresh_device_status(None, cx);
this
}
2026-02-24 15:01:51 +01:00
pub fn focus_handle(&self) -> FocusHandle {
self.focus_handle.clone()
}
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 {
return;
}
2026-03-05 23:19:07 +05:30
self.device.loading = true;
self.device.error = None;
cx.notify();
2026-01-29 17:42:03 +05:30
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);
2026-03-05 23:19:07 +05:30
self.device.error = None;
2026-01-29 17:42:03 +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-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
{
let device = self.device.clone();
2026-03-04 18:11:19 +05:30
config_view.update(cx, |view, cx| {
view.sync_from_device(&device, window, cx);
});
}
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-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();
}
}
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 {
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;
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-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()
.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(|| {
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 => {
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)
}
}