mirror of
https://github.com/librekeys/picoforge.git
synced 2026-07-28 08:01:19 -07:00
feat(ui): add config page skeleton
This commit is contained in:
+5
-1
@@ -15,4 +15,8 @@ vite.config.ts.timestamp-*
|
||||
|
||||
# Nix
|
||||
result
|
||||
.direnv
|
||||
.direnv
|
||||
|
||||
# misc
|
||||
docs-gpui-components
|
||||
src-svelte
|
||||
Generated
+3
-3
@@ -3002,7 +3002,7 @@ dependencies = [
|
||||
"rgb",
|
||||
"tiff",
|
||||
"zune-core 0.5.1",
|
||||
"zune-jpeg 0.5.11",
|
||||
"zune-jpeg 0.5.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8052,9 +8052,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zune-jpeg"
|
||||
version = "0.5.11"
|
||||
version = "0.5.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2959ca473aae96a14ecedf501d20b3608d2825ba280d5adb57d651721885b0c2"
|
||||
checksum = "410e9ecef634c709e3831c2cfdb8d9c32164fae1c67496d5b68fff728eec37fe"
|
||||
dependencies = [
|
||||
"zune-core 0.5.1",
|
||||
]
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ bitflags = "2.10"
|
||||
ring = "0.17" # For signing fido2 messages with pin token
|
||||
|
||||
# For Application UI:
|
||||
gpui = "0.2.2"
|
||||
gpui = { version = "0.2.2", features = [] }
|
||||
gpui-component = "0.5.0"
|
||||
rust-embed = "8.11.0"
|
||||
|
||||
|
||||
+1
-1
@@ -119,7 +119,7 @@ impl Render for ApplicationRoot {
|
||||
PasskeysView::build().into_any_element()
|
||||
}
|
||||
ActiveView::Configuration => {
|
||||
ConfigView::build().into_any_element()
|
||||
cx.new(|cx| ConfigView::new(window, cx)).into_any_element()
|
||||
}
|
||||
ActiveView::Security => {
|
||||
SecurityView::build(cx).into_any_element()
|
||||
|
||||
+443
-6
@@ -1,13 +1,450 @@
|
||||
// src/views/config.rs
|
||||
use crate::ui::components::page_view::PageView;
|
||||
use gpui::*;
|
||||
use gpui_component::{
|
||||
ActiveTheme, Disableable, Icon, StyledExt, Theme,
|
||||
button::Button,
|
||||
input::{Input, InputState},
|
||||
select::{Select, SelectItem, SelectState},
|
||||
slider::{Slider, SliderState},
|
||||
switch::Switch,
|
||||
v_flex,
|
||||
};
|
||||
|
||||
pub struct ConfigView;
|
||||
#[derive(Clone, PartialEq)]
|
||||
struct VendorItem {
|
||||
value: SharedString,
|
||||
label: SharedString,
|
||||
}
|
||||
|
||||
impl SelectItem for VendorItem {
|
||||
type Value = SharedString;
|
||||
|
||||
fn title(&self) -> SharedString {
|
||||
self.label.clone()
|
||||
}
|
||||
|
||||
fn value(&self) -> &Self::Value {
|
||||
&self.value
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
struct DriverItem {
|
||||
value: u8,
|
||||
label: SharedString,
|
||||
}
|
||||
|
||||
impl SelectItem for DriverItem {
|
||||
type Value = u8;
|
||||
|
||||
fn title(&self) -> SharedString {
|
||||
self.label.clone()
|
||||
}
|
||||
|
||||
fn value(&self) -> &Self::Value {
|
||||
&self.value
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ConfigView {
|
||||
vendor_select: Entity<SelectState<Vec<VendorItem>>>,
|
||||
vid_input: Entity<InputState>,
|
||||
pid_input: Entity<InputState>,
|
||||
product_name_input: Entity<InputState>,
|
||||
led_gpio_input: Entity<InputState>,
|
||||
led_driver_select: Entity<SelectState<Vec<DriverItem>>>,
|
||||
led_brightness_slider: Entity<SliderState>,
|
||||
led_dimmable: bool,
|
||||
led_steady: bool,
|
||||
touch_timeout_input: Entity<InputState>,
|
||||
power_cycle: bool,
|
||||
enable_secp256k1: bool,
|
||||
loading: bool,
|
||||
}
|
||||
|
||||
impl ConfigView {
|
||||
pub fn build() -> impl IntoElement {
|
||||
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let vendors = vec![
|
||||
VendorItem {
|
||||
value: "custom".into(),
|
||||
label: "Custom".into(),
|
||||
},
|
||||
VendorItem {
|
||||
value: "solokeys".into(),
|
||||
label: "SoloKeys".into(),
|
||||
},
|
||||
VendorItem {
|
||||
value: "google".into(),
|
||||
label: "Google".into(),
|
||||
},
|
||||
VendorItem {
|
||||
value: "yubico".into(),
|
||||
label: "Yubico".into(),
|
||||
},
|
||||
];
|
||||
|
||||
let drivers = vec![
|
||||
DriverItem {
|
||||
value: 0,
|
||||
label: "WS2812".into(),
|
||||
},
|
||||
DriverItem {
|
||||
value: 1,
|
||||
label: "SK6812".into(),
|
||||
},
|
||||
DriverItem {
|
||||
value: 2,
|
||||
label: "APA102".into(),
|
||||
},
|
||||
];
|
||||
|
||||
let vendor_select = cx.new(|cx| {
|
||||
SelectState::new(
|
||||
vendors,
|
||||
Some(gpui_component::IndexPath::default()),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let vid_input = cx.new(|cx| InputState::new(window, cx).default_value("CAFE"));
|
||||
let pid_input = cx.new(|cx| InputState::new(window, cx).default_value("4242"));
|
||||
let product_name_input = cx.new(|cx| InputState::new(window, cx).default_value("My Key"));
|
||||
|
||||
let led_gpio_input = cx.new(|cx| InputState::new(window, cx).default_value("25"));
|
||||
let led_driver_select = cx.new(|cx| {
|
||||
SelectState::new(
|
||||
drivers,
|
||||
Some(gpui_component::IndexPath::default()),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let led_brightness_slider = cx.new(|_| {
|
||||
SliderState::new()
|
||||
.min(0.0)
|
||||
.max(15.0)
|
||||
.step(1.0)
|
||||
.default_value(8.0)
|
||||
});
|
||||
|
||||
let touch_timeout_input = cx.new(|cx| InputState::new(window, cx).default_value("10"));
|
||||
|
||||
Self {
|
||||
vendor_select,
|
||||
vid_input,
|
||||
pid_input,
|
||||
product_name_input,
|
||||
led_gpio_input,
|
||||
led_driver_select,
|
||||
led_brightness_slider,
|
||||
led_dimmable: true,
|
||||
led_steady: false,
|
||||
touch_timeout_input,
|
||||
power_cycle: false,
|
||||
enable_secp256k1: true,
|
||||
loading: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn render_identity_card(&self, theme: &Theme) -> impl IntoElement {
|
||||
let content = v_flex()
|
||||
.gap_4()
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.child("Vendor Preset")
|
||||
.child(Select::new(&self.vendor_select).w_full()),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.grid()
|
||||
.grid_cols(2)
|
||||
.gap_4()
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.child("Vendor ID (HEX)")
|
||||
.child(Input::new(&self.vid_input).font_family("Mono")),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.child("Product ID (HEX)")
|
||||
.child(Input::new(&self.pid_input).font_family("Mono")),
|
||||
),
|
||||
)
|
||||
.child(div().h_px().bg(theme.border))
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.child("Product Name")
|
||||
.child(Input::new(&self.product_name_input)),
|
||||
);
|
||||
|
||||
Self::config_card(
|
||||
"Identity",
|
||||
"USB Identification settings",
|
||||
Icon::default().path("icons/tag.svg"),
|
||||
content,
|
||||
theme,
|
||||
)
|
||||
}
|
||||
|
||||
fn render_led_card(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let dim_listener = cx.listener(|this, checked, _, cx| {
|
||||
this.led_dimmable = *checked;
|
||||
cx.notify();
|
||||
});
|
||||
|
||||
let steady_listener = cx.listener(|this, checked, _, cx| {
|
||||
this.led_steady = *checked;
|
||||
cx.notify();
|
||||
});
|
||||
|
||||
// Access theme after creating listeners (which requires mutable borrow of cx)
|
||||
let theme = cx.theme();
|
||||
|
||||
// Read slider value (requires immutable borrow of cx)
|
||||
let brightness = self.led_brightness_slider.read(cx).value().start() as i32;
|
||||
|
||||
let content = v_flex()
|
||||
.gap_4()
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.child("LED GPIO Pin")
|
||||
.child(Input::new(&self.led_gpio_input)),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.child("LED Driver")
|
||||
.child(Select::new(&self.led_driver_select).w_full()),
|
||||
)
|
||||
.child(div().h_px().bg(theme.border))
|
||||
.child(
|
||||
v_flex().gap_2().child("Brightness (0-15)").child(
|
||||
gpui_component::h_flex()
|
||||
.items_center()
|
||||
.gap_4()
|
||||
.child(Slider::new(&self.led_brightness_slider).flex_1())
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.text_color(theme.muted_foreground)
|
||||
.child(format!("Level {}", brightness)),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
gpui_component::h_flex()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.child(
|
||||
v_flex().gap_0p5().child("LED Dimmable").child(
|
||||
div()
|
||||
.text_sm()
|
||||
.text_color(theme.muted_foreground)
|
||||
.child("Allow brightness adjustment"),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Switch::new("led-dimmable")
|
||||
.checked(self.led_dimmable)
|
||||
.on_click(dim_listener),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
gpui_component::h_flex()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.child(
|
||||
v_flex().gap_0p5().child("LED Steady Mode").child(
|
||||
div()
|
||||
.text_sm()
|
||||
.text_color(theme.muted_foreground)
|
||||
.child("Keep LED on constantly"),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Switch::new("led-steady")
|
||||
.checked(self.led_steady)
|
||||
.on_click(steady_listener),
|
||||
),
|
||||
);
|
||||
|
||||
Self::config_card(
|
||||
"LED Settings",
|
||||
"Adjust visual feedback behavior",
|
||||
Icon::default().path("icons/microchip.svg"),
|
||||
content,
|
||||
theme,
|
||||
)
|
||||
}
|
||||
|
||||
fn render_touch_card(&self, theme: &Theme) -> impl IntoElement {
|
||||
let content = v_flex().gap_4().child(
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.child("Touch Timeout (seconds)")
|
||||
.child(Input::new(&self.touch_timeout_input)),
|
||||
);
|
||||
|
||||
Self::config_card(
|
||||
"Touch & Timing",
|
||||
"Configure interaction timeouts",
|
||||
Icon::default().path("icons/settings.svg"),
|
||||
content,
|
||||
theme,
|
||||
)
|
||||
}
|
||||
|
||||
fn render_options_card(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let power_cycle_listener = cx.listener(|this, checked, _, cx| {
|
||||
this.power_cycle = *checked;
|
||||
cx.notify();
|
||||
});
|
||||
|
||||
let secp_listener = cx.listener(|this, checked, _, cx| {
|
||||
this.enable_secp256k1 = *checked;
|
||||
cx.notify();
|
||||
});
|
||||
|
||||
let theme = cx.theme();
|
||||
|
||||
let content = v_flex()
|
||||
.gap_4()
|
||||
.child(
|
||||
gpui_component::h_flex()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.child(
|
||||
v_flex().gap_0p5().child("Power Cycle on Reset").child(
|
||||
div()
|
||||
.text_sm()
|
||||
.text_color(theme.muted_foreground)
|
||||
.child("Restart device on reset"),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Switch::new("power-cycle")
|
||||
.checked(self.power_cycle)
|
||||
.on_click(power_cycle_listener),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
gpui_component::h_flex()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.child(
|
||||
v_flex().gap_0p5().child("Enable Secp256k1").child(
|
||||
div()
|
||||
.text_sm()
|
||||
.text_color(theme.muted_foreground)
|
||||
.child("Does not work on Android!"),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Switch::new("enable-secp")
|
||||
.checked(self.enable_secp256k1)
|
||||
.on_click(secp_listener),
|
||||
),
|
||||
);
|
||||
|
||||
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()
|
||||
.size_full()
|
||||
.p_8()
|
||||
.child("Passkey Management List goes here...")
|
||||
.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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ConfigView {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
// I need to call mutable methods first.
|
||||
let led_card = self.render_led_card(cx).into_any_element();
|
||||
let options_card = self.render_options_card(cx).into_any_element();
|
||||
|
||||
// Then get theme and render rest
|
||||
let theme = cx.theme();
|
||||
|
||||
let identity_card = self.render_identity_card(theme).into_any_element();
|
||||
let touch_card = self.render_touch_card(theme).into_any_element();
|
||||
|
||||
PageView::build(
|
||||
"Configuration",
|
||||
"Customize device settings and behavior.",
|
||||
v_flex()
|
||||
.gap_6()
|
||||
.child(
|
||||
div()
|
||||
.grid()
|
||||
.grid_cols(2)
|
||||
.gap_6()
|
||||
.child(identity_card)
|
||||
.child(led_card)
|
||||
.child(touch_card)
|
||||
.child(options_card),
|
||||
)
|
||||
.child(
|
||||
gpui_component::h_flex().justify_end().child(
|
||||
Button::new("apply-changes")
|
||||
.icon(Icon::default().path("icons/save.svg"))
|
||||
.child("Apply Changes")
|
||||
.disabled(self.loading)
|
||||
.on_click(|_, _, _| {
|
||||
println!("Save clicked");
|
||||
}),
|
||||
),
|
||||
),
|
||||
&theme,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user