mirror of
https://github.com/librekeys/picoforge.git
synced 2026-07-28 08:01:19 -07:00
chore(ui): convert buttons into a reusable component from entity
This commit is contained in:
+130
-88
@@ -4,23 +4,24 @@ use crate::ui::colors;
|
||||
use gpui::prelude::*;
|
||||
use gpui::*;
|
||||
use gpui_component::{
|
||||
Icon,
|
||||
Disableable, Icon, Sizable, Size,
|
||||
button::{Button, ButtonCustomVariant, ButtonVariants},
|
||||
h_flex,
|
||||
};
|
||||
|
||||
pub struct Clicked;
|
||||
|
||||
/// A stateless text button wrapper
|
||||
#[derive(IntoElement)]
|
||||
pub struct PFButton {
|
||||
text: SharedString,
|
||||
on_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut Context<Self>) + 'static>>,
|
||||
hovered: bool,
|
||||
hover_t: f32,
|
||||
on_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
|
||||
bg_color_start: Rgba,
|
||||
bg_color_hover: Rgba,
|
||||
bg_color_active: Rgba,
|
||||
width_full: bool,
|
||||
centered: bool,
|
||||
disabled: bool,
|
||||
small: bool,
|
||||
loading: bool,
|
||||
}
|
||||
|
||||
impl PFButton {
|
||||
@@ -28,24 +29,33 @@ impl PFButton {
|
||||
Self {
|
||||
text: text.into(),
|
||||
on_click: None,
|
||||
hovered: false,
|
||||
hover_t: 0.0,
|
||||
bg_color_start: rgb(0x1b1b1d),
|
||||
bg_color_hover: rgb(0x232325),
|
||||
bg_color_active: rgb(colors::zinc::ZINC700),
|
||||
width_full: false,
|
||||
centered: false,
|
||||
disabled: false,
|
||||
small: false,
|
||||
loading: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_click(
|
||||
mut self,
|
||||
handler: impl Fn(&ClickEvent, &mut Window, &mut Context<Self>) + 'static,
|
||||
handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
self.on_click = Some(Box::new(handler));
|
||||
self
|
||||
}
|
||||
|
||||
/// Allows overriding the default colors
|
||||
pub fn with_colors(mut self, start: Rgba, hover: Rgba, active: Rgba) -> Self {
|
||||
self.bg_color_start = start;
|
||||
self.bg_color_hover = hover;
|
||||
self.bg_color_active = active;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn section_header(mut self) -> Self {
|
||||
self.width_full = true;
|
||||
self.centered = false;
|
||||
@@ -61,34 +71,32 @@ impl PFButton {
|
||||
self.centered = true;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn disabled(mut self, disabled: bool) -> Self {
|
||||
self.disabled = disabled;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn small(mut self) -> Self {
|
||||
self.small = true;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn loading(mut self, loading: bool) -> Self {
|
||||
self.loading = loading;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for PFButton {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let target_hover = if self.hovered { 1.0 } else { 0.0 };
|
||||
if (self.hover_t - target_hover).abs() > 0.01 {
|
||||
self.hover_t += (target_hover - self.hover_t) * 0.2;
|
||||
window.request_animation_frame();
|
||||
} else {
|
||||
self.hover_t = target_hover;
|
||||
}
|
||||
|
||||
let c1 = self.bg_color_start;
|
||||
let c2 = self.bg_color_hover;
|
||||
let t = self.hover_t;
|
||||
let bg_color = Rgba {
|
||||
r: c1.r + (c2.r - c1.r) * t,
|
||||
g: c1.g + (c2.g - c1.g) * t,
|
||||
b: c1.b + (c2.b - c1.b) * t,
|
||||
a: 1.0,
|
||||
};
|
||||
|
||||
let text = self.text.clone();
|
||||
// RenderOnce makes this a "Component" not an "Entity"
|
||||
impl RenderOnce for PFButton {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let text = self.text;
|
||||
|
||||
let mut btn = Button::new("pf-btn").custom(
|
||||
ButtonCustomVariant::new(cx)
|
||||
.color(bg_color.into())
|
||||
.hover(bg_color.into())
|
||||
.color(self.bg_color_start.into())
|
||||
.hover(self.bg_color_hover.into())
|
||||
.active(self.bg_color_active.into()),
|
||||
);
|
||||
|
||||
@@ -96,37 +104,46 @@ impl Render for PFButton {
|
||||
btn = btn.w_full();
|
||||
}
|
||||
|
||||
if self.disabled {
|
||||
btn = btn.disabled(true);
|
||||
}
|
||||
|
||||
if self.loading {
|
||||
btn = btn.loading(true);
|
||||
}
|
||||
|
||||
if self.small {
|
||||
btn = btn.with_size(Size::Small);
|
||||
}
|
||||
|
||||
let content = if self.centered {
|
||||
h_flex().justify_center().child(text)
|
||||
} else {
|
||||
h_flex().child(text)
|
||||
};
|
||||
|
||||
let on_click = self.on_click.take();
|
||||
if let Some(handler) = on_click {
|
||||
btn = btn.on_click(cx.listener(move |_, event, window, cx| handler(event, window, cx)));
|
||||
if let Some(handler) = self.on_click {
|
||||
// We cast the handler to satisfy GPUI's generic requirements
|
||||
btn = btn.on_click(move |e, w, c| handler(e, w, c));
|
||||
}
|
||||
|
||||
div()
|
||||
.child(btn.child(content))
|
||||
.id("pf-btn-wrapper")
|
||||
.on_hover(cx.listener(|this, hovered, _, cx| {
|
||||
if this.hovered != *hovered {
|
||||
this.hovered = *hovered;
|
||||
cx.notify();
|
||||
}
|
||||
}))
|
||||
btn.child(content)
|
||||
}
|
||||
}
|
||||
|
||||
/// A stateless Icon + Text button wrapper
|
||||
#[derive(IntoElement)]
|
||||
pub struct PFIconButton {
|
||||
icon: Icon,
|
||||
text: SharedString,
|
||||
hovered: bool,
|
||||
hover_t: f32,
|
||||
on_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
|
||||
bg_color_start: Rgba,
|
||||
bg_color_hover: Rgba,
|
||||
bg_color_active: Rgba,
|
||||
disabled: bool,
|
||||
small: bool,
|
||||
width_full: bool,
|
||||
loading: bool,
|
||||
}
|
||||
|
||||
impl PFIconButton {
|
||||
@@ -134,60 +151,85 @@ impl PFIconButton {
|
||||
Self {
|
||||
icon: icon.into(),
|
||||
text: text.into(),
|
||||
hovered: false,
|
||||
hover_t: 0.0,
|
||||
on_click: None,
|
||||
bg_color_start: rgb(0x1b1b1d),
|
||||
bg_color_hover: rgb(0x232325),
|
||||
bg_color_active: rgb(colors::zinc::ZINC700),
|
||||
disabled: false,
|
||||
small: false,
|
||||
width_full: false,
|
||||
loading: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_click(
|
||||
mut self,
|
||||
handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
self.on_click = Some(Box::new(handler));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_colors(mut self, start: Rgba, hover: Rgba, active: Rgba) -> Self {
|
||||
self.bg_color_start = start;
|
||||
self.bg_color_hover = hover;
|
||||
self.bg_color_active = active;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn disabled(mut self, disabled: bool) -> Self {
|
||||
self.disabled = disabled;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn small(mut self) -> Self {
|
||||
self.small = true;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn w_full(mut self) -> Self {
|
||||
self.width_full = true;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn loading(mut self, loading: bool) -> Self {
|
||||
self.loading = loading;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<Clicked> for PFIconButton {}
|
||||
impl RenderOnce for PFIconButton {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let text = self.text;
|
||||
let icon = self.icon;
|
||||
|
||||
impl Render for PFIconButton {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let target_hover = if self.hovered { 1.0 } else { 0.0 };
|
||||
if (self.hover_t - target_hover).abs() > 0.01 {
|
||||
self.hover_t += (target_hover - self.hover_t) * 0.2;
|
||||
window.request_animation_frame();
|
||||
} else {
|
||||
self.hover_t = target_hover;
|
||||
let mut btn = Button::new("pf-icon-btn").custom(
|
||||
ButtonCustomVariant::new(cx)
|
||||
.color(self.bg_color_start.into())
|
||||
.hover(self.bg_color_hover.into())
|
||||
.active(self.bg_color_active.into()),
|
||||
);
|
||||
|
||||
if self.width_full {
|
||||
btn = btn.w_full();
|
||||
}
|
||||
|
||||
let c1 = self.bg_color_start;
|
||||
let c2 = self.bg_color_hover;
|
||||
let t = self.hover_t;
|
||||
let bg_color = Rgba {
|
||||
r: c1.r + (c2.r - c1.r) * t,
|
||||
g: c1.g + (c2.g - c1.g) * t,
|
||||
b: c1.b + (c2.b - c1.b) * t,
|
||||
a: 1.0,
|
||||
};
|
||||
if self.disabled {
|
||||
btn = btn.disabled(true);
|
||||
}
|
||||
|
||||
let text = self.text.clone();
|
||||
let icon = self.icon.clone();
|
||||
if self.loading {
|
||||
btn = btn.loading(true);
|
||||
}
|
||||
|
||||
let btn = Button::new("pf-icon-btn")
|
||||
.custom(
|
||||
ButtonCustomVariant::new(cx)
|
||||
.color(bg_color.into())
|
||||
.hover(bg_color.into())
|
||||
.active(self.bg_color_active.into()),
|
||||
)
|
||||
.w_full()
|
||||
.on_click(cx.listener(|_, _, _, cx| {
|
||||
cx.emit(Clicked);
|
||||
}));
|
||||
if self.small {
|
||||
btn = btn.with_size(Size::Small);
|
||||
}
|
||||
|
||||
div()
|
||||
.child(btn.child(h_flex().gap_2().justify_center().child(icon).child(text)))
|
||||
.id("pf-icon-btn-wrapper")
|
||||
.on_hover(cx.listener(|this, hovered, _, cx| {
|
||||
if this.hovered != *hovered {
|
||||
this.hovered = *hovered;
|
||||
cx.notify();
|
||||
}
|
||||
}))
|
||||
if let Some(handler) = self.on_click {
|
||||
btn = btn.on_click(move |e, w, c| handler(e, w, c));
|
||||
}
|
||||
|
||||
btn.child(h_flex().gap_2().justify_center().child(icon).child(text))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
use crate::device::types::DeviceMethod;
|
||||
use crate::ui::colors;
|
||||
use crate::ui::components::button::PFIconButton;
|
||||
use crate::ui::ui_types::{ActiveView, GlobalDeviceState};
|
||||
use gpui::*;
|
||||
use gpui_component::{ActiveTheme, Icon, IconName, Side, h_flex, sidebar::*, v_flex};
|
||||
use gpui_component::{
|
||||
ActiveTheme, Icon, IconName, Side,
|
||||
button::{Button, ButtonVariants},
|
||||
h_flex,
|
||||
sidebar::*,
|
||||
v_flex,
|
||||
};
|
||||
use std::rc::Rc;
|
||||
|
||||
pub struct AppSidebar<V: 'static> {
|
||||
@@ -11,8 +18,7 @@ pub struct AppSidebar<V: 'static> {
|
||||
collapsed: bool,
|
||||
state: GlobalDeviceState,
|
||||
on_select: Option<Rc<dyn Fn(&mut V, ActiveView, &mut Window, &mut Context<V>)>>,
|
||||
refresh_btn: Option<AnyElement>,
|
||||
refresh_btn_collapsed: Option<AnyElement>,
|
||||
on_refresh: Option<Rc<dyn Fn(&mut V, &mut Window, &mut Context<V>)>>,
|
||||
}
|
||||
|
||||
impl<V: 'static> AppSidebar<V> {
|
||||
@@ -28,8 +34,7 @@ impl<V: 'static> AppSidebar<V> {
|
||||
collapsed,
|
||||
state,
|
||||
on_select: None,
|
||||
refresh_btn: None,
|
||||
refresh_btn_collapsed: None,
|
||||
on_refresh: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,13 +46,11 @@ impl<V: 'static> AppSidebar<V> {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_refresh_btn(mut self, btn: impl IntoElement) -> Self {
|
||||
self.refresh_btn = Some(btn.into_any_element());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_refresh_btn_collapsed(mut self, btn: impl IntoElement) -> Self {
|
||||
self.refresh_btn_collapsed = Some(btn.into_any_element());
|
||||
pub fn on_refresh(
|
||||
mut self,
|
||||
handler: impl Fn(&mut V, &mut Window, &mut Context<V>) + 'static,
|
||||
) -> Self {
|
||||
self.on_refresh = Some(Rc::new(handler));
|
||||
self
|
||||
}
|
||||
|
||||
@@ -59,6 +62,10 @@ impl<V: 'static> AppSidebar<V> {
|
||||
let border_color = cx.theme().sidebar_border;
|
||||
let muted_foreground = cx.theme().muted_foreground;
|
||||
|
||||
// Clone for closures
|
||||
let on_refresh = self.on_refresh.clone();
|
||||
let on_refresh_collapsed = self.on_refresh.clone();
|
||||
|
||||
v_flex()
|
||||
.h_full()
|
||||
.bg(rgb(colors::zinc::ZINC900))
|
||||
@@ -176,7 +183,17 @@ impl<V: 'static> AppSidebar<V> {
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.gap_2()
|
||||
.children(self.refresh_btn_collapsed)
|
||||
.child(
|
||||
Button::new("refresh-btn-collapsed")
|
||||
.ghost()
|
||||
.child(Icon::default().path("icons/refresh-cw.svg"))
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
if let Some(f) = &on_refresh_collapsed {
|
||||
f(this, window, cx);
|
||||
}
|
||||
}))
|
||||
.w_full(),
|
||||
)
|
||||
.child(div().w(px(8.)).h(px(8.)).rounded_full().bg(
|
||||
if let Some(status) = &state.device_status {
|
||||
if status.method == DeviceMethod::Fido {
|
||||
@@ -235,7 +252,19 @@ impl<V: 'static> AppSidebar<V> {
|
||||
)
|
||||
}),
|
||||
)
|
||||
.children(self.refresh_btn)
|
||||
.child(
|
||||
PFIconButton::new(
|
||||
Icon::default().path("icons/refresh-cw.svg"),
|
||||
"Refresh",
|
||||
)
|
||||
.on_click(cx.listener(
|
||||
move |this, _, window, cx| {
|
||||
if let Some(f) = &on_refresh {
|
||||
f(this, window, cx);
|
||||
}
|
||||
},
|
||||
)),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
+6
-23
@@ -1,5 +1,4 @@
|
||||
use crate::device::io;
|
||||
use crate::ui::components::button::PFIconButton;
|
||||
use crate::ui::components::sidebar::AppSidebar;
|
||||
use crate::ui::ui_types::{ActiveView, GlobalDeviceState};
|
||||
use crate::ui::{
|
||||
@@ -13,7 +12,7 @@ use crate::ui::{
|
||||
use gpui::prelude::*;
|
||||
use gpui::*;
|
||||
use gpui_component::{
|
||||
ActiveTheme, Icon, IconName, TitleBar, WindowExt,
|
||||
ActiveTheme, IconName, TitleBar, WindowExt,
|
||||
button::{Button, ButtonVariants},
|
||||
h_flex,
|
||||
scroll::ScrollableElement,
|
||||
@@ -26,23 +25,13 @@ pub struct ApplicationRoot {
|
||||
state: GlobalDeviceState,
|
||||
device_loading: bool,
|
||||
sidebar_width: Pixels,
|
||||
refresh_button: Entity<PFIconButton>,
|
||||
config_view: Option<Entity<ConfigView>>,
|
||||
passkeys_view: Option<Entity<PasskeysView>>,
|
||||
}
|
||||
|
||||
impl ApplicationRoot {
|
||||
pub fn new(cx: &mut Context<Self>) -> Self {
|
||||
let refresh_button = cx
|
||||
.new(|_cx| PFIconButton::new(Icon::default().path("icons/refresh-cw.svg"), "Refresh"));
|
||||
|
||||
cx.subscribe(
|
||||
&refresh_button,
|
||||
|this, _, _: &crate::ui::components::button::Clicked, cx| {
|
||||
this.refresh_device_status(cx);
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
// We no longer need to create a persistent PFIconButton entity here
|
||||
|
||||
let mut this = Self {
|
||||
active_view: ActiveView::Home,
|
||||
@@ -50,7 +39,6 @@ impl ApplicationRoot {
|
||||
state: GlobalDeviceState::new(),
|
||||
device_loading: false,
|
||||
sidebar_width: px(255.),
|
||||
refresh_button,
|
||||
config_view: None,
|
||||
passkeys_view: None,
|
||||
};
|
||||
@@ -129,15 +117,10 @@ impl Render for ApplicationRoot {
|
||||
.on_select(|this: &mut Self, view, _, _| {
|
||||
this.active_view = view;
|
||||
})
|
||||
.with_refresh_btn(self.refresh_button.clone())
|
||||
.with_refresh_btn_collapsed(
|
||||
Button::new("refresh-btn-collapsed")
|
||||
.ghost()
|
||||
.child(Icon::default().path("icons/refresh-cw.svg"))
|
||||
.on_click(cx.listener(|this, _, _, cx| {
|
||||
this.refresh_device_status(cx);
|
||||
})),
|
||||
)
|
||||
// Here is the new connection: passing the refresh logic to the sidebar
|
||||
.on_refresh(|this, _, cx| {
|
||||
this.refresh_device_status(cx);
|
||||
})
|
||||
.render(cx),
|
||||
)
|
||||
.child(
|
||||
|
||||
+21
-36
@@ -1,6 +1,6 @@
|
||||
use crate::device::io;
|
||||
use crate::device::types::{FidoDeviceInfo, FullDeviceStatus, StoredCredential};
|
||||
use crate::ui::components::{card::Card, page_view::PageView};
|
||||
use crate::ui::components::{card::Card, page_view::PageView, button::{PFButton, PFIconButton}};
|
||||
use gpui::*;
|
||||
use gpui_component::button::{Button, ButtonVariants};
|
||||
use gpui_component::{
|
||||
@@ -275,9 +275,7 @@ impl PasskeysView {
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Button::new("change-pin-btn")
|
||||
.outline()
|
||||
.child(if pin_set { "Change PIN" } else { "Set PIN" })
|
||||
PFButton::new(if pin_set { "Change PIN" } else { "Set PIN" })
|
||||
.on_click(listener),
|
||||
)
|
||||
}
|
||||
@@ -315,10 +313,8 @@ impl PasskeysView {
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Button::new("update-pin-len-btn")
|
||||
.outline()
|
||||
.disabled(!pin_set)
|
||||
.child("Update Minimum Length"),
|
||||
PFButton::new("Update Minimum Length")
|
||||
.disabled(!pin_set),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -369,14 +365,11 @@ impl PasskeysView {
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Button::new("unlock-btn")
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(Icon::default().path("icons/lock-open.svg"))
|
||||
.child("Unlock Storage"),
|
||||
)
|
||||
.on_click(listener),
|
||||
PFIconButton::new(
|
||||
Icon::default().path("icons/lock-open.svg"),
|
||||
"Unlock Storage",
|
||||
)
|
||||
.on_click(listener),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -444,16 +437,12 @@ impl PasskeysView {
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Button::new("lock-storage-btn")
|
||||
.outline()
|
||||
.small()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(Icon::default().path("icons/lock.svg").size_3p5())
|
||||
.child("Lock Storage"),
|
||||
)
|
||||
.on_click(lock_listener),
|
||||
PFIconButton::new(
|
||||
Icon::default().path("icons/lock.svg").size_3p5(),
|
||||
"Lock Storage",
|
||||
)
|
||||
.small()
|
||||
.on_click(lock_listener),
|
||||
),
|
||||
)
|
||||
.child(if self.credentials.is_empty() {
|
||||
@@ -517,17 +506,15 @@ impl PasskeysView {
|
||||
.gap_2()
|
||||
.justify_end()
|
||||
.child(
|
||||
Button::new("cancel-unlock")
|
||||
.label("Cancel")
|
||||
PFButton::new("Cancel")
|
||||
.on_click(cx.listener(|this, _, _, cx| {
|
||||
this.active_modal = None;
|
||||
cx.notify();
|
||||
}))
|
||||
)
|
||||
.child(
|
||||
Button::new("confirm-unlock")
|
||||
.primary()
|
||||
.label("Unlock")
|
||||
PFButton::new("Unlock")
|
||||
// Primary/Zinc style is default
|
||||
.on_click(cx.listener(move |this, _, _, cx| {
|
||||
let pin = pin_input_clone.read(cx).text().to_string();
|
||||
if !pin.is_empty() {
|
||||
@@ -552,17 +539,15 @@ impl PasskeysView {
|
||||
.gap_2()
|
||||
.justify_end()
|
||||
.child(
|
||||
Button::new("cancel-del")
|
||||
.label("Cancel")
|
||||
PFButton::new("Cancel")
|
||||
.on_click(cx.listener(|this, _, _, cx| {
|
||||
this.active_modal = None;
|
||||
cx.notify();
|
||||
}))
|
||||
)
|
||||
.child(
|
||||
Button::new("confirm-del")
|
||||
.danger()
|
||||
.label("Delete")
|
||||
PFButton::new("Delete")
|
||||
.with_colors(rgb(0x7f1d1d), rgb(0x991b1b), rgb(0xfca5a5)) // Danger colors
|
||||
.on_click(cx.listener(move |this, _, _, cx| {
|
||||
this.execute_delete(cred_id.clone(), pin_str.clone(), cx);
|
||||
this.active_modal = None;
|
||||
|
||||
Reference in New Issue
Block a user