feat(ui): extract dialogs into a custom component

This commit is contained in:
Suyog Tandel
2026-02-21 22:46:22 +05:30
parent 0954fab7b4
commit 8dcac818ef
7 changed files with 343 additions and 298 deletions
Generated
+93 -93
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -16,7 +16,7 @@ directories = "6" # For Applcation config/data dir handling
# For device management backend:
pcsc = "2" # Standard Smart Card API (connect to the key)
hex = "0.4" # For parsing "CAFE:4242" VID/PID strings
hex = "0.4" # For parsing VID/PID strings
byteorder = "1.5" # Required for writing Big-Endian numbers (firmware requirement)
thiserror = "2" # Makes custom error handling much easier
anyhow = "1" # For easy error propagation
Generated
+3 -3
View File
@@ -20,11 +20,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1771008912,
"narHash": "sha256-gf2AmWVTs8lEq7z/3ZAsgnZDhWIckkb+ZnAo5RzSxJg=",
"lastModified": 1771369470,
"narHash": "sha256-0NBlEBKkN3lufyvFegY4TYv5mCNHbi5OmBDrzihbBMQ=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "a82ccc39b39b621151d6732718e3e250109076fa",
"rev": "0182a361324364ae3f436a63005877674cf45efb",
"type": "github"
},
"original": {
+185
View File
@@ -0,0 +1,185 @@
use gpui::*;
use gpui_component::{
WindowExt,
button::{Button, ButtonVariant, ButtonVariants},
dialog::DialogButtonProps,
input::{Input, InputState},
v_flex,
};
pub fn open_pin_prompt(
title: &str,
description: &str,
confirm_label: &str,
window: &mut Window,
cx: &mut App,
on_confirm: impl Fn(String, &mut Window, &mut App) + 'static,
) {
let title = SharedString::from(title.to_string());
let description = SharedString::from(description.to_string());
let confirm_label = SharedString::from(confirm_label.to_string());
let pin_input = cx.new(|cx| {
InputState::new(window, cx)
.placeholder("Enter FIDO PIN")
.masked(true)
});
let on_confirm = std::rc::Rc::new(on_confirm);
window.open_dialog(cx, move |dialog, _, _| {
let pin_input_for_footer = pin_input.clone();
let confirm_label = confirm_label.clone();
let on_confirm = on_confirm.clone();
dialog
.title(title.clone())
.child(
v_flex()
.gap_4()
.pb_4()
.child(description.clone())
.child(Input::new(&pin_input)),
)
.footer(move |_, _, _, _| {
let input = pin_input_for_footer.clone();
let on_confirm = on_confirm.clone();
vec![
Button::new("cancel")
.label("Cancel")
.on_click(|_, window, cx| {
window.close_dialog(cx);
}),
Button::new("confirm")
.primary()
.label(confirm_label.clone())
.on_click(move |_, window, cx| {
let pin = input.read(cx).text().to_string();
if !pin.is_empty() {
window.close_dialog(cx);
on_confirm(pin, window, cx);
}
}),
]
})
});
}
pub fn open_confirm(
title: &str,
message: String,
ok_label: &str,
ok_variant: ButtonVariant,
window: &mut Window,
cx: &mut App,
on_ok: impl Fn(&mut Window, &mut App) + 'static,
) {
let title = SharedString::from(title.to_string());
let ok_label = SharedString::from(ok_label.to_string());
let on_ok = std::rc::Rc::new(on_ok);
window.open_dialog(cx, move |dialog, _, _| {
let on_ok = on_ok.clone();
dialog
.confirm()
.title(title.clone())
.child(div().pb_4().child(message.clone()))
.on_ok(move |_, window, cx| {
on_ok(window, cx);
false
})
.on_cancel(|_, _, _| true)
.button_props(
DialogButtonProps::default()
.ok_text(ok_label.clone())
.ok_variant(ok_variant),
)
});
}
pub fn open_change_pin(
window: &mut Window,
cx: &mut App,
on_error: impl Fn(&str, &mut App) + 'static + Clone,
on_confirm: impl Fn(String, String, &mut App) + 'static,
) {
let current_pin = cx.new(|cx| {
InputState::new(window, cx)
.placeholder("Enter current PIN")
.masked(true)
});
let new_pin = cx.new(|cx| {
InputState::new(window, cx)
.placeholder("Enter new PIN")
.masked(true)
});
let confirm_pin = cx.new(|cx| {
InputState::new(window, cx)
.placeholder("Confirm new PIN")
.masked(true)
});
let on_confirm = std::rc::Rc::new(on_confirm);
window.open_dialog(cx, move |dialog, _, _| {
let current = current_pin.clone();
let new = new_pin.clone();
let confirm = confirm_pin.clone();
let on_error = on_error.clone();
let on_confirm = on_confirm.clone();
dialog
.title("Change PIN")
.child("Enter your current PIN and choose a new one.")
.child(
v_flex()
.gap_4()
.pb_4()
.child("Current PIN")
.child(Input::new(&current))
.child("New PIN")
.child(Input::new(&new))
.child("Confirm New PIN")
.child(Input::new(&confirm)),
)
.footer(move |_, _window, _cx, _| {
let current = current.clone();
let new = new.clone();
let confirm = confirm.clone();
let on_error = on_error.clone();
let on_confirm = on_confirm.clone();
vec![
Button::new("cancel")
.label("Cancel")
.on_click(|_, window, cx| window.close_dialog(cx)),
Button::new("confirm")
.primary()
.label("Confirm")
.on_click(move |_, _, cx| {
let current_val = current.read(cx).text().to_string();
let new_val = new.read(cx).text().to_string();
let confirm_val = confirm.read(cx).text().to_string();
if current_val.is_empty() {
return;
}
if new_val != confirm_val {
on_error("PINs do not match", cx);
return;
}
if new_val.len() < 4 {
on_error("PIN too short", cx);
return;
}
on_confirm(current_val, new_val, cx);
}),
]
})
});
}
+1
View File
@@ -1,4 +1,5 @@
pub mod button;
pub mod card;
pub mod dialog;
pub mod page_view;
pub mod sidebar;
+19 -51
View File
@@ -1,11 +1,11 @@
use crate::device::io;
use crate::device::types::{AppConfigInput, FullDeviceStatus};
use crate::ui::components::{card::Card, page_view::PageView};
use crate::ui::components::{card::Card, dialog, page_view::PageView};
use crate::ui::types::{LedDriverType, UsbIdentityPreset};
use gpui::*;
use gpui_component::button::{ButtonCustomVariant, ButtonVariants};
use gpui_component::{
ActiveTheme, Disableable, Icon, Theme, WindowExt,
ActiveTheme, Disableable, Icon, Theme,
button::Button,
input::{Input, InputState},
select::{Select, SelectItem, SelectState},
@@ -274,57 +274,25 @@ impl ConfigView {
window: &mut Window,
cx: &mut Context<Self>,
) {
let pin_input = cx.new(|cx| {
InputState::new(window, cx)
.placeholder("Enter FIDO PIN")
.masked(true)
});
let view_handle = cx.entity().downgrade();
window.open_dialog(cx, move |dialog, _, _| {
let view_handle_for_footer = view_handle.clone();
let pin_input_for_footer = pin_input.clone();
let changes = changes.clone();
dialog
.title("Authentication Required")
.child(
v_flex()
.gap_4()
.pb_4()
.child("Enter your device PIN to apply changes.")
.child(Input::new(&pin_input)),
)
.footer(move |_, _, _, _| {
let view = view_handle_for_footer.clone();
let input = pin_input_for_footer.clone();
let changes = changes.clone();
vec![
Button::new("cancel")
.label("Cancel")
.on_click(|_, window, cx| {
window.close_dialog(cx);
}),
Button::new("confirm").primary().label("Confirm").on_click(
move |_, window, cx| {
let pin = input.read(cx).text().to_string();
if !pin.is_empty() {
window.close_dialog(cx);
let _ = view.update(cx, |this, cx| {
this.write_config_to_device(
changes.clone(),
crate::device::types::DeviceMethod::Fido,
Some(pin),
cx,
);
});
}
},
),
]
})
});
dialog::open_pin_prompt(
"Authentication Required",
"Enter your device PIN to apply changes.",
"Confirm",
window,
cx,
move |pin, _, cx| {
let _ = view_handle.update(cx, |this, cx| {
this.write_config_to_device(
changes.clone(),
crate::device::types::DeviceMethod::Fido,
Some(pin),
cx,
);
});
},
);
}
fn apply_changes(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+41 -150
View File
@@ -3,6 +3,7 @@ use crate::device::types::{FidoDeviceInfo, FullDeviceStatus, StoredCredential};
use crate::ui::components::{
button::{PFButton, PFIconButton},
card::Card,
dialog,
page_view::PageView,
};
use gpui::*;
@@ -172,49 +173,20 @@ impl PasskeysView {
}
fn open_unlock_dialog(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let pin_input = cx.new(|cx| {
InputState::new(window, cx)
.placeholder("Enter FIDO PIN")
.masked(true)
});
let view_handle = cx.entity().downgrade();
window.open_dialog(cx, move |dialog, _, _| {
let view_handle_for_footer = view_handle.clone();
let pin_input_for_footer = pin_input.clone();
dialog
.title("Unlock Storage")
.child(
v_flex()
.gap_4()
.pb_4()
.child("Enter your device PIN to view saved passkeys")
.child(Input::new(&pin_input)),
)
.footer(move |_, _, _, _| {
let view = view_handle_for_footer.clone();
let input = pin_input_for_footer.clone();
vec![
Button::new("cancel")
.label("Cancel")
.on_click(|_, window, cx| {
window.close_dialog(cx);
}),
Button::new("unlock").primary().label("Unlock").on_click(
move |_, _window, cx| {
let pin = input.read(cx).text().to_string();
if !pin.is_empty() {
let _ = view.update(cx, |this, cx| {
this.unlock_storage(pin, cx);
});
}
},
),
]
})
});
dialog::open_pin_prompt(
"Unlock Storage",
"Enter your device PIN to view saved passkeys",
"Unlock",
window,
cx,
move |pin, _, cx| {
let _ = view_handle.update(cx, |this, cx| {
this.unlock_storage(pin, cx);
});
},
);
}
fn open_delete_dialog(
@@ -229,120 +201,39 @@ impl PasskeysView {
let name = cred.rp_id.clone();
let view_handle = cx.entity().downgrade();
window.open_dialog(cx, move |dialog, _, _| {
let view_handle = view_handle.clone();
let cred_id = cred_id.clone();
let pin_str = pin_str.clone();
dialog
.confirm()
.title("Delete Passkey")
.child(div().pb_4().child(format!(
"Are you sure you want to delete the passkey for {}?",
name
)))
.on_ok(move |_, _, cx| {
let _ = view_handle.update(cx, |this, cx| {
this.execute_delete(cred_id.clone(), pin_str.clone(), cx);
});
false
})
.on_cancel(|_, _, _| true)
.button_props(
gpui_component::dialog::DialogButtonProps::default()
.ok_text("Delete")
.ok_variant(ButtonVariant::Danger),
)
});
dialog::open_confirm(
"Delete Passkey",
format!("Are you sure you want to delete the passkey for {}?", name),
"Delete",
ButtonVariant::Danger,
window,
cx,
move |_, cx| {
let _ = view_handle.update(cx, |this, cx| {
this.execute_delete(cred_id.clone(), pin_str.clone(), cx);
});
},
);
}
fn open_change_pin_dialog(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let current_pin = cx.new(|cx| {
InputState::new(window, cx)
.placeholder("Enter current PIN")
.masked(true)
});
let new_pin = cx.new(|cx| {
InputState::new(window, cx)
.placeholder("Enter new PIN")
.masked(true)
});
let confirm_pin = cx.new(|cx| {
InputState::new(window, cx)
.placeholder("Confirm new PIN")
.masked(true)
});
let view_handle = cx.entity().downgrade();
let view_for_error = cx.entity().downgrade();
window.open_dialog(cx, move |dialog, _, _| {
let view = view_handle.clone();
let current = current_pin.clone();
let new = new_pin.clone();
let confirm = confirm_pin.clone();
dialog
.title("Change PIN")
.child("Enter your current PIN and choose a new one.")
.child(
v_flex()
.gap_4()
.pb_4()
.child("Current PIN")
.child(Input::new(&current))
.child("New PIN")
.child(Input::new(&new))
.child("Confirm New PIN")
.child(Input::new(&confirm)),
)
.footer(move |_, _window, _cx, _| {
let view = view.clone();
let current = current.clone();
let new = new.clone();
let confirm = confirm.clone();
vec![
Button::new("cancel")
.label("Cancel")
.on_click(|_, window, cx| window.close_dialog(cx)),
Button::new("confirm").primary().label("Confirm").on_click(
move |_, _, cx| {
let current_val = current.read(cx).text().to_string();
let new_val = new.read(cx).text().to_string();
let confirm_val = confirm.read(cx).text().to_string();
if current_val.is_empty() {
return;
// Todo: show error
}
if new_val != confirm_val {
// Todo: show validation error (toast)
let _ = view.update(cx, |_, cx| {
cx.emit(PasskeysEvent::Notification(
"PINs do not match".to_string(),
));
});
return;
}
if new_val.len() < 4 {
let _ = view.update(cx, |_, cx| {
cx.emit(PasskeysEvent::Notification(
"PIN too short".to_string(),
));
});
return;
}
let _ = view.update(cx, |this, cx| {
this.change_pin(current_val, new_val, cx);
});
},
),
]
})
});
dialog::open_change_pin(
window,
cx,
move |msg, cx| {
let _ = view_for_error.update(cx, |_, cx| {
cx.emit(PasskeysEvent::Notification(msg.to_string()));
});
},
move |current, new, cx| {
let _ = view_handle.update(cx, |this, cx| {
this.change_pin(current, new, cx);
});
},
);
}
fn open_min_pin_length_dialog(&mut self, window: &mut Window, cx: &mut Context<Self>) {