add backup panel
Some checks failed
Rust / build (ubuntu-latest, stable) (push) Failing after 1m47s
Rust / build (ubuntu-latest, stable) (pull_request) Failing after 1m52s
Rust / build (macos-latest, stable) (push) Has been cancelled
Rust / build (windows-latest, stable) (push) Has been cancelled
Rust / build (macos-latest, stable) (pull_request) Has been cancelled
Rust / build (windows-latest, stable) (pull_request) Has been cancelled
Some checks failed
Rust / build (ubuntu-latest, stable) (push) Failing after 1m47s
Rust / build (ubuntu-latest, stable) (pull_request) Failing after 1m52s
Rust / build (macos-latest, stable) (push) Has been cancelled
Rust / build (windows-latest, stable) (push) Has been cancelled
Rust / build (macos-latest, stable) (pull_request) Has been cancelled
Rust / build (windows-latest, stable) (pull_request) Has been cancelled
This commit is contained in:
194
crates/coop/src/panels/backup.rs
Normal file
194
crates/coop/src/panels/backup.rs
Normal file
@@ -0,0 +1,194 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Error;
|
||||
use gpui::{
|
||||
div, AnyElement, App, AppContext, ClipboardItem, Context, Entity, EventEmitter, FocusHandle,
|
||||
Focusable, IntoElement, ParentElement, Render, SharedString, Styled, Task, Window,
|
||||
};
|
||||
use nostr_sdk::prelude::*;
|
||||
use state::KEYRING;
|
||||
use theme::ActiveTheme;
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
||||
use ui::input::{InputState, TextInput};
|
||||
use ui::{divider, v_flex, IconName, Sizable, StyledExt};
|
||||
|
||||
const MSG: &str = "Store your account keys in a safe location. \
|
||||
You can restore your account or move to another client anytime you want.";
|
||||
|
||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<BackupPanel> {
|
||||
cx.new(|cx| BackupPanel::new(window, cx))
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct BackupPanel {
|
||||
name: SharedString,
|
||||
focus_handle: FocusHandle,
|
||||
|
||||
/// Public key input
|
||||
npub_input: Entity<InputState>,
|
||||
|
||||
/// Secret key input
|
||||
nsec_input: Entity<InputState>,
|
||||
|
||||
/// Copied status
|
||||
copied: bool,
|
||||
|
||||
/// Background tasks
|
||||
tasks: Vec<Task<Result<(), Error>>>,
|
||||
}
|
||||
|
||||
impl BackupPanel {
|
||||
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let npub_input = cx.new(|cx| InputState::new(window, cx).disabled(true));
|
||||
let nsec_input = cx.new(|cx| InputState::new(window, cx).disabled(true));
|
||||
|
||||
// Run at the end of current cycle
|
||||
cx.defer_in(window, |this, window, cx| {
|
||||
this.load(window, cx);
|
||||
});
|
||||
|
||||
Self {
|
||||
name: "Backup".into(),
|
||||
focus_handle: cx.focus_handle(),
|
||||
npub_input,
|
||||
nsec_input,
|
||||
copied: false,
|
||||
tasks: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn load(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let keyring = cx.read_credentials(KEYRING);
|
||||
|
||||
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
|
||||
if let Some((_, secret)) = keyring.await? {
|
||||
let secret = SecretKey::from_slice(&secret)?;
|
||||
let keys = Keys::new(secret);
|
||||
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
this.npub_input.update(cx, |this, cx| {
|
||||
this.set_value(keys.public_key().to_bech32().unwrap(), window, cx);
|
||||
});
|
||||
|
||||
this.nsec_input.update(cx, |this, cx| {
|
||||
this.set_value(keys.secret_key().to_bech32().unwrap(), window, cx);
|
||||
});
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}));
|
||||
}
|
||||
|
||||
fn copy_secret_key(&mut self, cx: &mut Context<Self>) {
|
||||
let value = self.nsec_input.read(cx).value();
|
||||
let item = ClipboardItem::new_string(value.to_string());
|
||||
|
||||
// Copy to clipboard
|
||||
cx.write_to_clipboard(item);
|
||||
|
||||
// Set the copied status to true
|
||||
self.set_copied(true, cx);
|
||||
}
|
||||
|
||||
fn set_copied(&mut self, status: bool, cx: &mut Context<Self>) {
|
||||
self.copied = status;
|
||||
cx.notify();
|
||||
|
||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||
cx.background_executor().timer(Duration::from_secs(2)).await;
|
||||
|
||||
// Clear the error message after a delay
|
||||
this.update(cx, |this, cx| {
|
||||
this.set_copied(false, cx);
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
impl Panel for BackupPanel {
|
||||
fn panel_id(&self) -> SharedString {
|
||||
self.name.clone()
|
||||
}
|
||||
|
||||
fn title(&self, _cx: &App) -> AnyElement {
|
||||
self.name.clone().into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<PanelEvent> for BackupPanel {}
|
||||
|
||||
impl Focusable for BackupPanel {
|
||||
fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for BackupPanel {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
.p_3()
|
||||
.gap_3()
|
||||
.w_full()
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child(SharedString::from(MSG)),
|
||||
)
|
||||
.child(divider(cx))
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.flex_1()
|
||||
.w_full()
|
||||
.text_sm()
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_1p5()
|
||||
.w_full()
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.font_semibold()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child(SharedString::from("Public Key:")),
|
||||
)
|
||||
.child(TextInput::new(&self.npub_input).small().bordered(false)),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_1p5()
|
||||
.w_full()
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.font_semibold()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child(SharedString::from("Secret Key:")),
|
||||
)
|
||||
.child(TextInput::new(&self.nsec_input).small().bordered(false)),
|
||||
)
|
||||
.child(
|
||||
Button::new("copy")
|
||||
.icon(IconName::Copy)
|
||||
.label({
|
||||
if self.copied {
|
||||
"Copied"
|
||||
} else {
|
||||
"Copy secret key"
|
||||
}
|
||||
})
|
||||
.primary()
|
||||
.small()
|
||||
.font_semibold()
|
||||
.on_click(cx.listener(move |this, _ev, _window, cx| {
|
||||
this.copy_secret_key(cx);
|
||||
})),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -144,16 +144,17 @@ impl MessagingRelayPanel {
|
||||
self.error = Some(error.into());
|
||||
cx.notify();
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
|
||||
cx.background_executor().timer(Duration::from_secs(2)).await;
|
||||
|
||||
// Clear the error message after a delay
|
||||
this.update(cx, |this, cx| {
|
||||
this.error = None;
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.detach();
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}));
|
||||
}
|
||||
|
||||
fn set_updating(&mut self, updating: bool, cx: &mut Context<Self>) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod backup;
|
||||
pub mod connect;
|
||||
pub mod encryption_key;
|
||||
pub mod greeter;
|
||||
|
||||
@@ -163,16 +163,17 @@ impl RelayListPanel {
|
||||
self.error = Some(error.into());
|
||||
cx.notify();
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
|
||||
cx.background_executor().timer(Duration::from_secs(2)).await;
|
||||
|
||||
// Clear the error message after a delay
|
||||
this.update(cx, |this, cx| {
|
||||
this.error = None;
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.detach();
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}));
|
||||
}
|
||||
|
||||
fn set_updating(&mut self, updating: bool, cx: &mut Context<Self>) {
|
||||
|
||||
@@ -20,7 +20,7 @@ use ui::dock_area::{ClosePanel, DockArea, DockItem};
|
||||
use ui::menu::DropdownMenu;
|
||||
use ui::{h_flex, v_flex, IconName, Root, Sizable, WindowExtension};
|
||||
|
||||
use crate::panels::{encryption_key, greeter, messaging_relays, relay_list};
|
||||
use crate::panels::{backup, encryption_key, greeter, messaging_relays, profile, relay_list};
|
||||
use crate::sidebar;
|
||||
|
||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Workspace> {
|
||||
@@ -30,11 +30,17 @@ pub fn init(window: &mut Window, cx: &mut App) -> Entity<Workspace> {
|
||||
#[derive(Action, Clone, PartialEq, Eq, Deserialize)]
|
||||
#[action(namespace = workspace, no_json)]
|
||||
enum Command {
|
||||
ReloadRelayList,
|
||||
OpenRelayPanel,
|
||||
ReloadInbox,
|
||||
OpenInboxPanel,
|
||||
OpenEncryptionPanel,
|
||||
ToggleTheme,
|
||||
|
||||
RefreshRelayList,
|
||||
RefreshMessagingRelays,
|
||||
|
||||
ShowRelayList,
|
||||
ShowMessaging,
|
||||
ShowEncryption,
|
||||
ShowProfile,
|
||||
ShowSettings,
|
||||
ShowBackup,
|
||||
}
|
||||
|
||||
pub struct Workspace {
|
||||
@@ -181,7 +187,32 @@ impl Workspace {
|
||||
|
||||
fn on_command(&mut self, command: &Command, window: &mut Window, cx: &mut Context<Self>) {
|
||||
match command {
|
||||
Command::OpenEncryptionPanel => {
|
||||
Command::ShowProfile => {
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let signer = nostr.read(cx).signer();
|
||||
|
||||
if let Some(public_key) = signer.public_key() {
|
||||
self.dock.update(cx, |this, cx| {
|
||||
this.add_panel(
|
||||
Arc::new(profile::init(public_key, window, cx)),
|
||||
DockPlacement::Right,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
Command::ShowBackup => {
|
||||
self.dock.update(cx, |this, cx| {
|
||||
this.add_panel(
|
||||
Arc::new(backup::init(window, cx)),
|
||||
DockPlacement::Right,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
Command::ShowEncryption => {
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let signer = nostr.read(cx).signer();
|
||||
|
||||
@@ -196,7 +227,7 @@ impl Workspace {
|
||||
});
|
||||
}
|
||||
}
|
||||
Command::OpenInboxPanel => {
|
||||
Command::ShowMessaging => {
|
||||
self.dock.update(cx, |this, cx| {
|
||||
this.add_panel(
|
||||
Arc::new(messaging_relays::init(window, cx)),
|
||||
@@ -206,7 +237,7 @@ impl Workspace {
|
||||
);
|
||||
});
|
||||
}
|
||||
Command::OpenRelayPanel => {
|
||||
Command::ShowRelayList => {
|
||||
self.dock.update(cx, |this, cx| {
|
||||
this.add_panel(
|
||||
Arc::new(relay_list::init(window, cx)),
|
||||
@@ -216,18 +247,19 @@ impl Workspace {
|
||||
);
|
||||
});
|
||||
}
|
||||
Command::ReloadInbox => {
|
||||
Command::RefreshRelayList => {
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
nostr.update(cx, |this, cx| {
|
||||
this.ensure_relay_list(cx);
|
||||
});
|
||||
}
|
||||
Command::ReloadRelayList => {
|
||||
Command::RefreshMessagingRelays => {
|
||||
let chat = ChatRegistry::global(cx);
|
||||
chat.update(cx, |this, cx| {
|
||||
this.ensure_messaging_relays(cx);
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -252,12 +284,29 @@ impl Workspace {
|
||||
.compact()
|
||||
.transparent()
|
||||
.dropdown_menu(move |this, _window, _cx| {
|
||||
this.label(profile.name())
|
||||
this.min_w(px(256.))
|
||||
.label(profile.name())
|
||||
.separator()
|
||||
.menu("Profile", Box::new(ClosePanel))
|
||||
.menu("Backup", Box::new(ClosePanel))
|
||||
.menu("Themes", Box::new(ClosePanel))
|
||||
.menu("Settings", Box::new(ClosePanel))
|
||||
.menu_with_icon(
|
||||
"Profile",
|
||||
IconName::Profile,
|
||||
Box::new(Command::ShowProfile),
|
||||
)
|
||||
.menu_with_icon(
|
||||
"Backup",
|
||||
IconName::UserKey,
|
||||
Box::new(Command::ShowBackup),
|
||||
)
|
||||
.menu_with_icon(
|
||||
"Themes",
|
||||
IconName::Sun,
|
||||
Box::new(Command::ToggleTheme),
|
||||
)
|
||||
.menu_with_icon(
|
||||
"Settings",
|
||||
IconName::Settings,
|
||||
Box::new(Command::ShowSettings),
|
||||
)
|
||||
}),
|
||||
)
|
||||
})
|
||||
@@ -298,7 +347,7 @@ impl Workspace {
|
||||
.small()
|
||||
.ghost()
|
||||
.on_click(|_ev, window, cx| {
|
||||
window.dispatch_action(Box::new(Command::OpenEncryptionPanel), cx);
|
||||
window.dispatch_action(Box::new(Command::ShowEncryption), cx);
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
@@ -333,7 +382,7 @@ impl Workspace {
|
||||
this.min_w(px(260.))
|
||||
.label("Messaging Relays")
|
||||
.menu_element_with_disabled(
|
||||
Box::new(Command::OpenRelayPanel),
|
||||
Box::new(Command::ShowRelayList),
|
||||
true,
|
||||
move |_window, cx| {
|
||||
let persons = PersonRegistry::global(cx);
|
||||
@@ -380,12 +429,12 @@ impl Workspace {
|
||||
.menu_with_icon(
|
||||
"Reload",
|
||||
IconName::Refresh,
|
||||
Box::new(Command::ReloadInbox),
|
||||
Box::new(Command::RefreshMessagingRelays),
|
||||
)
|
||||
.menu_with_icon(
|
||||
"Update relays",
|
||||
IconName::Settings,
|
||||
Box::new(Command::OpenInboxPanel),
|
||||
Box::new(Command::ShowMessaging),
|
||||
)
|
||||
}),
|
||||
),
|
||||
@@ -421,7 +470,7 @@ impl Workspace {
|
||||
this.min_w(px(260.))
|
||||
.label("Relays")
|
||||
.menu_element_with_disabled(
|
||||
Box::new(Command::OpenRelayPanel),
|
||||
Box::new(Command::ShowRelayList),
|
||||
true,
|
||||
move |_window, cx| {
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
@@ -465,12 +514,12 @@ impl Workspace {
|
||||
.menu_with_icon(
|
||||
"Reload",
|
||||
IconName::Refresh,
|
||||
Box::new(Command::ReloadRelayList),
|
||||
Box::new(Command::RefreshRelayList),
|
||||
)
|
||||
.menu_with_icon(
|
||||
"Update relay list",
|
||||
IconName::Settings,
|
||||
Box::new(Command::OpenRelayPanel),
|
||||
Box::new(Command::ShowRelayList),
|
||||
)
|
||||
}),
|
||||
),
|
||||
|
||||
@@ -25,61 +25,3 @@ pub async fn upload(server: Url, path: PathBuf, cx: &AsyncApp) -> Result<Url, Er
|
||||
.await
|
||||
.map_err(|e| anyhow!("Upload error: {e}"))?
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_mime_type_detection() {
|
||||
// Test common file extensions
|
||||
assert_eq!(
|
||||
from_path("image.jpg").first_or_octet_stream().to_string(),
|
||||
"image/jpeg"
|
||||
);
|
||||
assert_eq!(
|
||||
from_path("document.pdf")
|
||||
.first_or_octet_stream()
|
||||
.to_string(),
|
||||
"application/pdf"
|
||||
);
|
||||
assert_eq!(
|
||||
from_path("page.html").first_or_octet_stream().to_string(),
|
||||
"text/html"
|
||||
);
|
||||
assert_eq!(
|
||||
from_path("data.json").first_or_octet_stream().to_string(),
|
||||
"application/json"
|
||||
);
|
||||
assert_eq!(
|
||||
from_path("script.js").first_or_octet_stream().to_string(),
|
||||
"text/javascript"
|
||||
);
|
||||
assert_eq!(
|
||||
from_path("style.css").first_or_octet_stream().to_string(),
|
||||
"text/css"
|
||||
);
|
||||
|
||||
// Test unknown extension falls back to octet-stream
|
||||
assert_eq!(
|
||||
from_path("unknown.xyz").first_or_octet_stream().to_string(),
|
||||
"chemical/x-xyz"
|
||||
);
|
||||
|
||||
// Test no extension falls back to octet-stream
|
||||
assert_eq!(
|
||||
from_path("file_without_extension")
|
||||
.first_or_octet_stream()
|
||||
.to_string(),
|
||||
"application/octet-stream"
|
||||
);
|
||||
|
||||
// Test truly unknown extension
|
||||
assert_eq!(
|
||||
from_path("unknown.unknown123")
|
||||
.first_or_octet_stream()
|
||||
.to_string(),
|
||||
"application/octet-stream"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -544,16 +544,12 @@ impl InputState {
|
||||
/// Set the text of the input field.
|
||||
///
|
||||
/// And the selection_range will be reset to 0..0.
|
||||
pub fn set_value(
|
||||
&mut self,
|
||||
value: impl Into<SharedString>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
pub fn set_value<T>(&mut self, value: T, window: &mut Window, cx: &mut Context<Self>)
|
||||
where
|
||||
T: Into<SharedString>,
|
||||
{
|
||||
self.history.ignore = true;
|
||||
let was_disabled = self.disabled;
|
||||
self.replace_text(value, window, cx);
|
||||
self.disabled = was_disabled;
|
||||
self.history.ignore = false;
|
||||
|
||||
// Ensure cursor to start when set text
|
||||
@@ -565,48 +561,50 @@ impl InputState {
|
||||
|
||||
// Move scroll to top
|
||||
self.scroll_handle.set_offset(point(px(0.), px(0.)));
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Insert text at the current cursor position.
|
||||
///
|
||||
/// And the cursor will be moved to the end of inserted text.
|
||||
pub fn insert(
|
||||
&mut self,
|
||||
text: impl Into<SharedString>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
pub fn insert<T>(&mut self, text: T, window: &mut Window, cx: &mut Context<Self>)
|
||||
where
|
||||
T: Into<SharedString>,
|
||||
{
|
||||
let was_disabled = self.disabled;
|
||||
self.disabled = false;
|
||||
let text: SharedString = text.into();
|
||||
let range_utf16 = self.range_to_utf16(&(self.cursor()..self.cursor()));
|
||||
self.replace_text_in_range_silent(Some(range_utf16), &text, window, cx);
|
||||
self.selected_range = (self.selected_range.end..self.selected_range.end).into();
|
||||
self.disabled = was_disabled;
|
||||
}
|
||||
|
||||
/// Replace text at the current cursor position.
|
||||
///
|
||||
/// And the cursor will be moved to the end of replaced text.
|
||||
pub fn replace(
|
||||
&mut self,
|
||||
text: impl Into<SharedString>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
pub fn replace<T>(&mut self, text: T, window: &mut Window, cx: &mut Context<Self>)
|
||||
where
|
||||
T: Into<SharedString>,
|
||||
{
|
||||
let was_disabled = self.disabled;
|
||||
self.disabled = false;
|
||||
let text: SharedString = text.into();
|
||||
self.replace_text_in_range_silent(None, &text, window, cx);
|
||||
self.selected_range = (self.selected_range.end..self.selected_range.end).into();
|
||||
self.disabled = was_disabled;
|
||||
}
|
||||
|
||||
fn replace_text(
|
||||
&mut self,
|
||||
text: impl Into<SharedString>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
fn replace_text<T>(&mut self, text: T, window: &mut Window, cx: &mut Context<Self>)
|
||||
where
|
||||
T: Into<SharedString>,
|
||||
{
|
||||
let was_disabled = self.disabled;
|
||||
self.disabled = false;
|
||||
let text: SharedString = text.into();
|
||||
let range = 0..self.text.chars().map(|c| c.len_utf16()).sum();
|
||||
self.replace_text_in_range_silent(Some(range), &text, window, cx);
|
||||
self.disabled = was_disabled;
|
||||
}
|
||||
|
||||
/// Set with password masked state.
|
||||
|
||||
@@ -93,8 +93,7 @@ impl RenderOnce for MenuItemElement {
|
||||
.id(self.id)
|
||||
.group(&self.group_name)
|
||||
.gap_x_1()
|
||||
.py_1()
|
||||
.px_2()
|
||||
.p_1()
|
||||
.text_sm()
|
||||
.text_color(cx.theme().text)
|
||||
.relative()
|
||||
|
||||
@@ -1073,7 +1073,7 @@ impl PopupMenu {
|
||||
|
||||
let selected = self.selected_index == Some(ix);
|
||||
const EDGE_PADDING: Pixels = px(4.);
|
||||
const INNER_PADDING: Pixels = px(8.);
|
||||
const INNER_PADDING: Pixels = px(4.);
|
||||
|
||||
let is_submenu = matches!(item, PopupMenuItem::Submenu { .. });
|
||||
let group_name = format!("{}:item-{}", cx.entity().entity_id(), ix);
|
||||
@@ -1143,7 +1143,7 @@ impl PopupMenu {
|
||||
.flex_1()
|
||||
.min_h(item_height)
|
||||
.items_center()
|
||||
.gap_x_1()
|
||||
.gap_x_2()
|
||||
.children(Self::render_icon(
|
||||
has_left_icon,
|
||||
is_left_check,
|
||||
|
||||
Reference in New Issue
Block a user