diff --git a/src/commands/bitwarden/accounts.rs b/src/commands/bitwarden/accounts.rs index 0641bd7..b2ada4b 100644 --- a/src/commands/bitwarden/accounts.rs +++ b/src/commands/bitwarden/accounts.rs @@ -9,7 +9,7 @@ use crate::{ commands::{RootCommand, RootCommandBuilder}, components::{ form::{Form, Input, InputKind}, - list::{Accessory, Item, ListBuilder, ListItem}, + list::{Accessory, Item, ItemBuilder, ListBuilder, ListItem}, shared::{Icon, Img}, }, state::{Action, Shortcut, StateModel, StateViewBuilder, StateViewContext}, @@ -227,71 +227,60 @@ impl StateViewBuilder for BitwardenAccountListBuilder { .into_iter() .map(|account| { let account = account.contents; - Item::new( - account.id.clone(), - vec![account.id.clone()], - cx.new_view({ - let id = account.id.clone(); - let instance = account.instance.clone(); - move |_| { - ListItem::new( - Some(Img::list_icon(Icon::User, None)), - id, - None, - vec![Accessory::new(instance, None)], - ) - } - }) - .into(), - None, - vec![ - Action::new( - Img::list_icon(Icon::Pen, None), - "Edit", - None, - { - // TODO: - move |actions, cx| { - actions.toast.error("Not implemented", cx); + ItemBuilder::new(account.id.clone(), { + let id = account.id.clone(); + let instance = account.instance.clone(); + ListItem::new( + Some(Img::list_icon(Icon::User, None)), + id, + None, + vec![Accessory::new(instance, None)], + ) + }) + .keywords(vec![account.id.clone()]) + .actions(vec![ + Action::new( + Img::list_icon(Icon::Pen, None), + "Edit", + None, + { + // TODO: + move |actions, cx| { + actions.toast.error("Not implemented", cx); + } + }, + false, + ), + Action::new( + Img::list_icon(Icon::Delete, None), + "Delete", + None, + { + // + let path = account.path(); + let id = account.id.clone(); + move |actions, cx| { + if let Err(err) = fs::remove_dir_all(path.clone()) { + error!("Failed to delete account: {}", err); + actions.toast.error("Failed to delete account", cx); } - }, - false, - ), - Action::new( - Img::list_icon(Icon::Delete, None), - "Delete", - None, - { - // - let path = account.path(); - let id = account.id.clone(); - move |actions, cx| { - if let Err(err) = fs::remove_dir_all(path.clone()) { + if let Some(account) = + BitwardenAccount::get(&id, db()).unwrap() + { + if let Err(err) = account.delete(db()) { error!("Failed to delete account: {}", err); actions .toast .error("Failed to delete account", cx); } - if let Some(account) = - BitwardenAccount::get(&id, db()).unwrap() - { - if let Err(err) = account.delete(db()) { - error!("Failed to delete account: {}", err); - actions - .toast - .error("Failed to delete account", cx); - } - }; - StateModel::update(|this, cx| this.reset(cx), cx); - } - }, - false, - ), - ], - None, - None, - None, - ) + }; + StateModel::update(|this, cx| this.reset(cx), cx); + } + }, + false, + ), + ]) + .build() }) .collect(); Ok(Some(items)) diff --git a/src/commands/bitwarden/list.rs b/src/commands/bitwarden/list.rs index 582ed83..761d7d6 100644 --- a/src/commands/bitwarden/list.rs +++ b/src/commands/bitwarden/list.rs @@ -18,7 +18,7 @@ use url::Url; use crate::{ commands::{RootCommand, RootCommandBuilder}, components::{ - list::{Accessory, AsyncListItems, Item, ListBuilder, ListItem}, + list::{Accessory, AsyncListItems, Item, ItemBuilder, ListBuilder, ListItem}, shared::{Icon, Img}, }, db::Db, @@ -464,25 +464,20 @@ impl RootCommandBuilder for BitwardenCommandBuilder { // StateItem::init(BitwardenAccountListBuilder, false, cx) // }).ok(); actions.append(&mut login.get_actions(&id, &account)); - items.push(Item::new( - id.clone(), - keywords, - cx.new_view(|_| { + items.push( + ItemBuilder::new( + id.clone(), ListItem::new( Some(img), name.clone(), None, vec![Accessory::new(login.username.clone(), None)], - ) - }) - .unwrap() - .into(), - None, - actions, - None, - None, - None, - )); + ), + ) + .keywords(keywords) + .actions(actions) + .build(), + ); } } } else { diff --git a/src/commands/clipboard/list.rs b/src/commands/clipboard/list.rs index c8aa4a4..0a8b45f 100644 --- a/src/commands/clipboard/list.rs +++ b/src/commands/clipboard/list.rs @@ -24,7 +24,7 @@ use tz::TimeZone; use crate::{ commands::{RootCommand, RootCommandBuilder}, components::{ - list::{AsyncListItems, Item, ListBuilder, ListItem}, + list::{AsyncListItems, Item, ItemBuilder, ListBuilder, ListItem}, shared::{Icon, Img, ImgMask, ImgSize, ImgSource}, }, db::Db, @@ -91,15 +91,7 @@ impl StateViewBuilder for ClipboardListBuilder { items.get(&t).cloned().unwrap_or_default() }; - items.sort_by_key(|item| { - Reverse( - item.meta - .value() - .downcast_ref::() - .unwrap() - .copied_last, - ) - }); + items.sort_by_key(|item| Reverse(item.get_meta::())); Ok(Some(items)) }, None, @@ -199,103 +191,92 @@ impl ClipboardListItem { item } fn get_item(&self, cx: &mut ViewContext) -> Item { - Item::new( + ItemBuilder::new( self.id, - vec![self.title.clone()], - cx.new_view(|_| { - ListItem::new( - match self.kind.clone() { - ClipboardListItemKind::Image { thumbnail } => { - Some(Img::list_file(thumbnail)) + ListItem::new( + match self.kind.clone() { + ClipboardListItemKind::Image { thumbnail } => Some(Img::list_file(thumbnail)), + _ => Some(Img::list_icon(Icon::File, None)), + }, + self.title.clone(), + None, + vec![], + ), + ) + .keywords(vec![self.title.clone()]) + .preview(0.66, { + let id = self.id; + move |cx| StateItem::init(ClipboardPreview::init(id, cx), false, cx) + }) + .actions({ + let mut actions = vec![ + Action::new( + Img::list_icon(Icon::ClipboardPaste, None), + "Paste", + None, + { + let id = self.id; + move |_, cx| { + let detail = ClipboardDetail::get(&id, db_detail()).unwrap().unwrap(); + let _ = cx.update_window(cx.window_handle(), |_, cx| { + match detail.contents.kind.clone() { + ClipboardKind::Text { text, .. } => { + close_and_paste(text.as_str(), false, cx); + } + ClipboardKind::Image { path, .. } => { + close_and_paste_file(&path, cx); + } + } + }); } - _ => Some(Img::list_icon(Icon::File, None)), }, - self.title.clone(), + false, + ), + Action::new( + Img::list_icon(Icon::Trash, None), + "Delete", None, - vec![], - ) - }) - .into(), - Some(( - 0.66, - Box::new({ - let id = self.id; - move |cx| StateItem::init(ClipboardPreview::init(id, cx), false, cx) - }), - )), - { - let mut actions = vec![ - Action::new( - Img::list_icon(Icon::ClipboardPaste, None), - "Paste", - None, - { - let id = self.id; - move |_, cx| { - let detail = - ClipboardDetail::get(&id, db_detail()).unwrap().unwrap(); - let _ = - cx.update_window(cx.window_handle(), |_, cx| { - match detail.contents.kind.clone() { - ClipboardKind::Text { text, .. } => { - close_and_paste(text.as_str(), false, cx); - } - ClipboardKind::Image { path, .. } => { - close_and_paste_file(&path, cx); - } - } - }); + { + let self_clone = self.clone(); + let view = cx.view().clone(); + move |actions, cx| { + if let Err(err) = self_clone.delete(view.downgrade(), cx) { + error!("Failed to delete clipboard entry: {:?}", err); + actions.toast.error("Failed to delete clipboard entry", cx); + } else { + actions + .toast + .success("Successfully deleted clipboard entry", cx); } - }, - false, - ), + } + }, + false, + ), + ]; + if let ClipboardListItemKind::Image { thumbnail } = self.kind.clone() { + actions.insert( + 1, Action::new( - Img::list_icon(Icon::Trash, None), - "Delete", + Img::list_icon(Icon::ScanEye, None), + "Copy Text to Clipboard", None, { - let self_clone = self.clone(); - let view = cx.view().clone(); + let mut path = thumbnail.clone(); + path.pop(); + path = path.join(format!("{}.png", self.id)); move |actions, cx| { - if let Err(err) = self_clone.delete(view.downgrade(), cx) { - error!("Failed to delete clipboard entry: {:?}", err); - actions.toast.error("Failed to delete clipboard entry", cx); - } else { - actions - .toast - .success("Successfully deleted clipboard entry", cx); - } + get_text_from_image(&path); + actions.toast.success("Copied Text to Clipboard", cx); } }, false, ), - ]; - if let ClipboardListItemKind::Image { thumbnail } = self.kind.clone() { - actions.insert( - 1, - Action::new( - Img::list_icon(Icon::ScanEye, None), - "Copy Text to Clipboard", - None, - { - let mut path = thumbnail.clone(); - path.pop(); - path = path.join(format!("{}.png", self.id)); - move |actions, cx| { - get_text_from_image(&path); - actions.toast.success("Copied Text to Clipboard", cx); - } - }, - false, - ), - ) - } - actions - }, - None, - Some(Box::new(self.clone())), - None, - ) + ) + } + actions + }) + .meta(self.copied_last) + .build() } fn delete(&self, view: WeakView, cx: &mut WindowContext) -> anyhow::Result<()> { let _ = view.update(cx, |view, cx| { diff --git a/src/commands/matrix/chat.rs b/src/commands/matrix/chat.rs index 3d8cecf..40522bf 100644 --- a/src/commands/matrix/chat.rs +++ b/src/commands/matrix/chat.rs @@ -29,8 +29,8 @@ use url::Url; use crate::{ components::{ - list::{AsyncListItems, Item, ListBuilder}, - shared::{Icon, Img, ImgMask, NoView}, + list::{AsyncListItems, Item, ItemBuilder, ItemComponent, ItemPreset, ListBuilder}, + shared::{Icon, Img, ImgMask}, }, state::{Action, Shortcut, StateViewBuilder, StateViewContext}, theme::Theme, @@ -181,7 +181,48 @@ pub(super) struct Message { } impl Message { - fn render(&mut self, selected: bool, cx: &WindowContext) -> AnyElement { + fn actions(&self, _client: &Client, _cx: &mut AsyncWindowContext) -> Vec { + let mut actions = vec![Action::new( + Img::list_icon(Icon::MessageCircleReply, None), + "Reply", + Some(Shortcut::cmd("r")), + move |_, _cx| { + info!("Reply to message"); + }, + false, + )]; + if self.me { + actions.append(&mut vec![ + Action::new( + Img::list_icon(Icon::MessageCircleMore, None), + "Edit", + Some(Shortcut::cmd("e")), + move |_, _cx| { + info!("Edit message"); + }, + false, + ), + Action::new( + Img::list_icon(Icon::MessageCircleDashed, None), + "Delete", + Some(Shortcut::cmd("backspace")), + move |_, _cx| { + info!("Delete message"); + }, + false, + ), + ]) + } + // + actions + } +} + +impl ItemComponent for Message { + fn clone_box(&self) -> Box { + Box::new(self.clone()) + } + fn render(&self, selected: bool, cx: &WindowContext) -> AnyElement { let theme = cx.global::(); let show_avatar = !self.me && self.first; let show_reactions = !self.reactions.inner.is_empty(); @@ -269,41 +310,6 @@ impl Message { ) .into_any_element() } - fn actions(&self, _client: &Client, _cx: &mut AsyncWindowContext) -> Vec { - let mut actions = vec![Action::new( - Img::list_icon(Icon::MessageCircleReply, None), - "Reply", - Some(Shortcut::cmd("r")), - move |_, _cx| { - info!("Reply to message"); - }, - false, - )]; - if self.me { - actions.append(&mut vec![ - Action::new( - Img::list_icon(Icon::MessageCircleMore, None), - "Edit", - Some(Shortcut::cmd("e")), - move |_, _cx| { - info!("Edit message"); - }, - false, - ), - Action::new( - Img::list_icon(Icon::MessageCircleDashed, None), - "Delete", - Some(Shortcut::cmd("backspace")), - move |_, _cx| { - info!("Delete message"); - }, - false, - ), - ]) - } - // - actions - } } fn get_source( @@ -535,19 +541,12 @@ async fn sync( let items: Vec = messages .into_iter() .map(|m| { - Item::new( - m.id.clone(), - vec![m.sender.clone()], - cx.new_view(|_| NoView).unwrap().into(), - None, - m.actions(&client, cx), - None, - Some(Box::new(m)), - Some(|this, selected, cx| { - let message: &Message = this.meta.value().downcast_ref().unwrap(); - message.clone().render(selected, cx) - }), - ) + ItemBuilder::new(m.id.clone(), m.clone()) + .keywords(vec![m.sender.clone()]) + .actions(m.actions(&client, cx)) + .meta(m) + .preset(ItemPreset::Plain) + .build() }) .collect(); @@ -620,7 +619,7 @@ impl StateViewBuilder for ChatRoom { .into_iter() .filter(|item| { let text = this.query.get_text(cx).to_lowercase(); - let message: &Message = item.meta.value().downcast_ref().unwrap(); + let message: &Message = item.get_meta(); if let MessageContent::Text(t) = &message.content { if t.to_lowercase().contains(&text) { return true; diff --git a/src/commands/matrix/list.rs b/src/commands/matrix/list.rs index 524b272..d8021d8 100644 --- a/src/commands/matrix/list.rs +++ b/src/commands/matrix/list.rs @@ -11,12 +11,10 @@ use matrix_sdk::{ use crate::{ commands::{RootCommand, RootCommandBuilder}, components::{ - list::{AsyncListItems, Item, ListBuilder, ListItem}, + list::{AsyncListItems, Item, ItemBuilder, ListBuilder, ListItem}, shared::{Icon, Img, ImgMask}, }, - state::{ - Action, Shortcut, StateItem, StateModel, StateViewBuilder, StateViewContext, - }, + state::{Action, Shortcut, StateItem, StateModel, StateViewBuilder, StateViewContext}, }; use super::{ @@ -69,7 +67,7 @@ impl StateViewBuilder for RoomList { items.get(&account).cloned().unwrap_or_default() }; items.sort_unstable_by_key(|item| { - let timestamp = item.meta.value().downcast_ref::().cloned().unwrap(); + let timestamp: u64 = item.get_meta(); Reverse(timestamp) }); Ok(Some(items)) @@ -152,58 +150,50 @@ async fn sync( preview }; - Item::new( + ItemBuilder::new( room_id.clone(), - vec![name.clone()], - cx.new_view(|_| ListItem::new(Some(img), name.clone(), None, vec![])) - .unwrap() - .into(), - Some(( - 0.66, - Box::new(move |cx| StateItem::init(preview.clone(), false, cx)), - )), - vec![ - Action::new( - Img::list_icon(Icon::MessageCircle, None), - "Write", - None, - { - let client = client.clone(); - let room_id = room_id.clone(); - move |_, cx| { - let item = StateItem::init( - Compose::new( - client.clone(), - room_id.clone(), - ComposeKind::Message, - ), - false, - cx, - ); - StateModel::update(|this, cx| this.push_item(item, cx), cx); - } - }, - false, - ), - Action::new( - Img::list_icon(Icon::Search, None), - "Search", - Some(Shortcut::cmd("/")), - |actions, cx| { - StateModel::update( - |this, cx| { - this.push_item(actions.active.clone().unwrap(), cx) - }, + ListItem::new(Some(img), name.clone(), None, vec![]), + ) + .keywords(vec![name.clone()]) + .actions(vec![ + Action::new( + Img::list_icon(Icon::MessageCircle, None), + "Write", + None, + { + let client = client.clone(); + let room_id = room_id.clone(); + move |_, cx| { + let item = StateItem::init( + Compose::new( + client.clone(), + room_id.clone(), + ComposeKind::Message, + ), + false, cx, ); - }, - false, - ), - ], - None, - Some(Box::new(timestamp)), - None, - ) + StateModel::update(|this, cx| this.push_item(item, cx), cx); + } + }, + false, + ), + Action::new( + Img::list_icon(Icon::Search, None), + "Search", + Some(Shortcut::cmd("/")), + |actions, cx| { + StateModel::update( + |this, cx| this.push_item(actions.active.clone().unwrap(), cx), + cx, + ); + }, + false, + ), + ]) + .preview(0.66, move |cx| StateItem::init(preview.clone(), false, cx)) + .meta(timestamp) + .build() }) .collect(); diff --git a/src/commands/mod.rs b/src/commands/mod.rs index bdb07b9..4c283f2 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -6,7 +6,7 @@ use log::error; use crate::{ components::{ form::{Form, Input, InputKind}, - list::{Accessory, Item, ListItem}, + list::{Accessory, Item, ItemBuilder, ListItem}, shared::{Icon, Img}, }, hotkey::HotkeyManager, @@ -99,52 +99,47 @@ impl RootCommands { .map(|command| { let mut keywords = vec![command.title.clone(), command.subtitle.clone()]; keywords.append(&mut command.keywords.clone()); - Item::new( + ItemBuilder::new( command.id.clone(), - keywords, - cx.new_view(|_| { - ListItem::new( - Some(Img::list_icon(command.icon.clone(), None)), - command.title.clone(), - Some(command.subtitle.clone()), - command - .shortcut - .clone() - .map(|shortcut| vec![Accessory::shortcut(shortcut)]) - .unwrap_or(vec![Accessory::new("Command", None)]), - ) - }) - .into(), - None, - vec![ - Action::new( - Img::list_icon(command.icon.clone(), None), - command.title.clone(), - None, - command.action.clone(), - false, - ), - Action::new( - Img::list_icon(Icon::Keyboard, None), - "Change Hotkey", - None, - { - let id = command.id.clone(); - move |_, cx| { - let id = id.clone(); - StateModel::update( - |this, cx| this.push(HotkeyBuilder { id }, cx), - cx, - ); - } - }, - false, - ), - ], - Some(3), - None, - None, + ListItem::new( + Some(Img::list_icon(command.icon.clone(), None)), + command.title.clone(), + Some(command.subtitle.clone()), + command + .shortcut + .clone() + .map(|shortcut| vec![Accessory::shortcut(shortcut)]) + .unwrap_or(vec![Accessory::new("Command", None)]), + ), ) + .keywords(keywords) + .actions(vec![ + Action::new( + Img::list_icon(command.icon.clone(), None), + command.title.clone(), + None, + command.action.clone(), + false, + ), + Action::new( + Img::list_icon(Icon::Keyboard, None), + "Change Hotkey", + None, + { + let id = command.id.clone(); + move |_, cx| { + let id = id.clone(); + StateModel::update( + |this, cx| this.push(HotkeyBuilder { id }, cx), + cx, + ); + } + }, + false, + ), + ]) + .weight(3) + .build() }) .collect(); items diff --git a/src/commands/root/list.rs b/src/commands/root/list.rs index f63898c..0577dad 100644 --- a/src/commands/root/list.rs +++ b/src/commands/root/list.rs @@ -5,7 +5,7 @@ use gpui::*; use crate::{ commands::{RootCommand, RootCommandBuilder, RootCommands}, components::{ - list::{nucleo::fuzzy_match, Accessory, Item, ListBuilder, ListItem}, + list::{nucleo::fuzzy_match, Accessory, Item, ItemBuilder, ListBuilder, ListItem}, shared::{Icon, Img}, }, platform::get_app_data, @@ -13,7 +13,7 @@ use crate::{ window::Window, }; -use super::numbat::Numbat; +use super::numbat::{Numbat, NumbatWrapper}; #[derive(Clone)] pub struct RootListBuilder; @@ -74,68 +74,62 @@ impl StateViewBuilder for RootListBuilder { continue; } let data = data.unwrap(); - let app = Item::new( + let app = ItemBuilder::new( data.id.clone(), - vec![data.name.clone()], - cx.new_view(|_cx| { - ListItem::new( - Some(data.icon.clone()), - data.name.clone(), - None, - vec![Accessory::new(data.tag.clone(), None)], - ) - }) - .into(), - None, - vec![Action::new( - Img::list_icon(Icon::ArrowUpRightFromSquare, None), - format!("Open {}", data.tag.clone()), + ListItem::new( + Some(data.icon.clone()), + data.name.clone(), None, - { - let id = data.id.clone(); + vec![Accessory::new(data.tag.clone(), None)], + ), + ) + .keywords(vec![data.name.clone()]) + .actions(vec![Action::new( + Img::list_icon(Icon::ArrowUpRightFromSquare, None), + format!("Open {}", data.tag.clone()), + None, + { + let id = data.id.clone(); - #[cfg(target_os = "macos")] - { - let ex = data.tag == "System Setting"; - move |_, cx| { - Window::close(cx); - let id = id.clone(); - let mut command = - std::process::Command::new("open"); - if ex { - command.arg(format!( - "x-apple.systempreferences:{}", - id - )); - } else { - command.arg("-b"); - command.arg(id); - } - let _ = command.spawn(); + #[cfg(target_os = "macos")] + { + let ex = data.tag == "System Setting"; + move |_, cx| { + Window::close(cx); + let id = id.clone(); + let mut command = + std::process::Command::new("open"); + if ex { + command.arg(format!( + "x-apple.systempreferences:{}", + id + )); + } else { + command.arg("-b"); + command.arg(id); } + let _ = command.spawn(); } - #[cfg(target_os = "linux")] - { - move |_, cx| { - Window::close(cx); - let mut command = - std::process::Command::new("gtk-launch"); - command.arg(id.clone()); - let _ = command.spawn(); - } + } + #[cfg(target_os = "linux")] + { + move |_, cx| { + Window::close(cx); + let mut command = + std::process::Command::new("gtk-launch"); + command.arg(id.clone()); + let _ = command.spawn(); } - }, - false, - )], - None, - None, - None, - ); + } + }, + false, + )]) + .build(); apps.insert(data.id, app); } } let mut apps: Vec = apps.values().cloned().collect(); - apps.sort_unstable_by_key(|a| a.keywords[0].clone()); + apps.sort_unstable_by_key(|a| a.get_keywords()[0].clone()); Ok(Some(apps)) } }, @@ -150,12 +144,14 @@ impl StateViewBuilder for RootListBuilder { let mut items = fuzzy_match(&query, items, false); if items.is_empty() { if let Some(result) = numbat.read(cx).result.clone() { - items.push(Item::new( - "Numbat", - Vec::::new(), - numbat.clone().into(), - None, - vec![Action::new( + items.push( + ItemBuilder::new( + "Numbat", + NumbatWrapper { + inner: numbat.clone(), + }, + ) + .actions(vec![Action::new( Img::list_icon(Icon::Copy, None), "Copy", None, @@ -173,11 +169,9 @@ impl StateViewBuilder for RootListBuilder { } }, false, - )], - None, - None, - None, - )); + )]) + .build(), + ); } } items diff --git a/src/commands/root/menu.rs b/src/commands/root/menu.rs index 25e08ea..114f617 100644 --- a/src/commands/root/menu.rs +++ b/src/commands/root/menu.rs @@ -8,7 +8,7 @@ use swift_rs::{swift, SRData}; use crate::{ commands::{RootCommand, RootCommandBuilder}, components::{ - list::{Accessory, Item, ListBuilder, ListItem}, + list::{Accessory, ItemBuilder, ListBuilder, ListItem}, shared::{Icon, Img}, }, state::{Action, Shortcut, StateModel, StateViewBuilder, StateViewContext}, @@ -73,19 +73,17 @@ impl StateViewBuilder for MenuListBuilder { vec![] }; - Item::new( - path.clone(), - vec![name.clone(), subtitle.clone()], - cx.new_view(|_| { - ListItem::new(None, name, Some(subtitle), accessories) - }) - .into(), - None, - actions, - None, - None, - None, - ) + ItemBuilder::new(path.clone(), { + ListItem::new( + None, + name.clone(), + Some(subtitle.clone()), + accessories, + ) + }) + .keywords(vec![name.clone(), subtitle.clone()]) + .actions(actions) + .build() }) .collect(), )) diff --git a/src/commands/root/numbat.rs b/src/commands/root/numbat.rs index 4ae54d5..d7a384f 100644 --- a/src/commands/root/numbat.rs +++ b/src/commands/root/numbat.rs @@ -7,7 +7,7 @@ use numbat::{ }; use crate::{ - components::shared::Icon, + components::{list::ItemComponent, shared::Icon}, query::{TextEvent, TextInputWeak}, theme::Theme, }; @@ -107,6 +107,20 @@ impl Numbat { } } +#[derive(Clone)] +pub struct NumbatWrapper { + pub inner: View, +} + +impl ItemComponent for NumbatWrapper { + fn clone_box(&self) -> Box { + Box::new(self.clone()) + } + fn render(&self, selected: bool, cx: &WindowContext) -> AnyElement { + self.inner.clone().into_any_element() + } +} + impl Render for Numbat { fn render(&mut self, cx: &mut gpui::ViewContext) -> impl IntoElement { let theme = cx.global::(); diff --git a/src/commands/root/process.rs b/src/commands/root/process.rs index 4ba4154..bb7fcc1 100644 --- a/src/commands/root/process.rs +++ b/src/commands/root/process.rs @@ -1,7 +1,6 @@ use gpui::*; use std::{ - cmp::Reverse, collections::HashMap, fs, path::PathBuf, process::Command, - time::Duration, + cmp::Reverse, collections::HashMap, fs, path::PathBuf, process::Command, time::Duration, }; use regex::Regex; @@ -9,7 +8,7 @@ use regex::Regex; use crate::{ commands::{RootCommand, RootCommandBuilder}, components::{ - list::{Accessory, Item, ListBuilder, ListItem}, + list::{Accessory, ItemBuilder, ListBuilder, ListItem}, shared::{Icon, Img}, }, paths::paths, @@ -135,60 +134,52 @@ impl StateViewBuilder for ProcessListBuilder { keywords: vec![], tag: "".to_string(), }); - - Item::new( - p.pid, - vec![data.name.clone()], - cx.new_view(|_cx| { - let (m, c) = if sort_by_cpu { - (None, Some(lavender)) - } else { - (Some(lavender), None) - }; - ListItem::new( - Some(data.icon), - data.name.clone(), - None, - vec![ - Accessory::new( - format!("{: >8}", format_bytes(p.mem * 1024)), - Some(Img::accessory_icon(Icon::MemoryStick, m)), - ), - Accessory::new( - format!("{: >6.2}%", p.cpu), - Some(Img::accessory_icon(Icon::Cpu, c)), - ), - ], - ) - }) - .into(), - None, - vec![Action::new( - Img::list_icon(Icon::Skull, None), - "Kill Process", + ItemBuilder::new(p.pid, { + let (m, c) = if sort_by_cpu { + (None, Some(lavender)) + } else { + (Some(lavender), None) + }; + ListItem::new( + Some(data.icon), + data.name.clone(), None, - { - let pid = p.pid.to_string(); - move |this, cx| { - if Command::new("kill") - .arg("-9") - .arg(pid.clone()) - .output() - .is_err() - { - this.toast.error("Failed to kill process", cx); - } else { - this.toast.success("Killed process", cx); - } - this.update(); - } - }, - false, - )], + vec![ + Accessory::new( + format!("{: >8}", format_bytes(p.mem * 1024)), + Some(Img::accessory_icon(Icon::MemoryStick, m)), + ), + Accessory::new( + format!("{: >6.2}%", p.cpu), + Some(Img::accessory_icon(Icon::Cpu, c)), + ), + ], + ) + }) + .keywords(vec![data.name.clone()]) + .actions(vec![Action::new( + Img::list_icon(Icon::Skull, None), + "Kill Process", None, - None, - None, - ) + { + let pid = p.pid.to_string(); + move |this, cx| { + if Command::new("kill") + .arg("-9") + .arg(pid.clone()) + .output() + .is_err() + { + this.toast.error("Failed to kill process", cx); + } else { + this.toast.success("Killed process", cx); + } + this.update(); + } + }, + false, + )]) + .build() }) .collect(), )) diff --git a/src/commands/root/theme.rs b/src/commands/root/theme.rs index b25f78f..e2a1d00 100644 --- a/src/commands/root/theme.rs +++ b/src/commands/root/theme.rs @@ -5,7 +5,7 @@ use gpui::*; use crate::{ commands::{RootCommand, RootCommandBuilder}, components::{ - list::{Item, ListBuilder, ListItem}, + list::{ItemBuilder, ListBuilder, ListItem}, shared::{Icon, Img}, }, db::db, @@ -27,98 +27,86 @@ impl StateViewBuilder for ThemeListBuilder { themes .into_iter() .map(|theme| { - Item::new( + ItemBuilder::new( theme.name.clone(), - vec![theme.name.clone()], - cx.new_view(|_| { - ListItem::new( - Some(Img::list_dot(theme.base)), - theme.name.clone(), - None, - vec![], - ) - }) - .into(), - None, - vec![ - Action::new( - Img::list_icon(Icon::Palette, None), - "Select Theme", - None, - { - let theme = theme.clone(); - move |this, cx| { - cx.update_global::(|this, _| { - *this = theme.clone(); - }); - this.toast.success("Theme activated", cx); - cx.refresh(); + ListItem::new( + Some(Img::list_dot(theme.base)), + theme.name.clone(), + None, + vec![], + ), + ) + .keywords(vec![theme.name.clone()]) + .actions(vec![ + Action::new( + Img::list_icon(Icon::Palette, None), + "Select Theme", + None, + { + let theme = theme.clone(); + move |this, cx| { + cx.update_global::(|this, _| { + *this = theme.clone(); + }); + this.toast.success("Theme activated", cx); + cx.refresh(); + } + }, + false, + ), + Action::new( + Img::list_icon(Icon::Sun, None), + "Default Light Theme", + Some(Shortcut::cmd("l")), + { + let name = theme.name.clone(); + move |this, cx| { + let mut settings = db() + .get::("theme") + .unwrap_or_default(); + settings.light = name.clone().to_string(); + if db() + .set::("theme", &settings) + .is_err() + { + this.toast + .error("Failed to change light theme", cx); + } else { + this.toast.success("Changed light theme", cx); } - }, - false, - ), - Action::new( - Img::list_icon(Icon::Sun, None), - "Default Light Theme", - Some(Shortcut::cmd("l")), - { - let name = theme.name.clone(); - move |this, cx| { - let mut settings = db() - .get::("theme") - .unwrap_or_default(); - settings.light = name.clone().to_string(); - if db() - .set::("theme", &settings) - .is_err() - { - this.toast.error( - "Failed to change light theme", - cx, - ); - } else { - this.toast - .success("Changed light theme", cx); - } - cx.refresh(); - } - }, - false, - ), - Action::new( - Img::list_icon(Icon::Moon, None), - "Default Dark Theme", - Some(Shortcut::cmd("d")), - { - let name = theme.name.clone(); - move |this, cx| { - let mut settings = db() - .get::("theme") - .unwrap_or_default(); - settings.dark = name.clone().to_string(); - if db() - .set::("theme", &settings) - .is_err() - { - this.toast.error( - "Failed to change dark theme", - cx, - ); - } else { - this.toast - .success("Changed dark theme", cx); - } - cx.refresh(); + cx.refresh(); + } + }, + false, + ), + Action::new( + Img::list_icon(Icon::Moon, None), + "Default Dark Theme", + Some(Shortcut::cmd("d")), + { + let name = theme.name.clone(); + move |this, cx| { + let mut settings = db() + .get::("theme") + .unwrap_or_default(); + settings.dark = name.clone().to_string(); + if db() + .set::("theme", &settings) + .is_err() + { + this.toast + .error("Failed to change dark theme", cx); + } else { + this.toast.success("Changed dark theme", cx); } - }, - false, - ), - ], - None, - None, - None, - ) + cx.refresh(); + } + }, + false, + ), + ]) + .build() }) .collect(), )) diff --git a/src/commands/tailscale/list.rs b/src/commands/tailscale/list.rs index a89883f..5795641 100644 --- a/src/commands/tailscale/list.rs +++ b/src/commands/tailscale/list.rs @@ -7,7 +7,7 @@ use time::OffsetDateTime; use crate::{ commands::{RootCommand, RootCommandBuilder}, components::{ - list::{Accessory, Item, ListBuilder, ListItem}, + list::{Accessory, Item, ItemBuilder, ListBuilder, ListItem}, shared::{Icon, Img}, }, state::{Action, Shortcut, StateModel, StateViewBuilder, StateViewContext}, @@ -84,20 +84,17 @@ impl StateViewBuilder for TailscaleListBuilder { let ip = p.tailscale_ips.first().unwrap(); let ipv6 = p.tailscale_ips.last().unwrap(); let url = format!("https://{}", &ip); - Some(Item::new( - p.id.clone(), - vec![name], - cx.new_view(|_| { + Some( + ItemBuilder::new( + p.id.clone(), ListItem::new( Some(Img::list_dot(color)), name, Some(p.os.to_string()), vec![Accessory::Tag { tag, img: None }], - ) - }) - .into(), - None, - vec![ + ), + ) + .actions(vec![ Action::new( Img::list_icon(Icon::ArrowUpRightFromSquare, None), "Open", @@ -158,14 +155,13 @@ impl StateViewBuilder for TailscaleListBuilder { }, false, ), - ], - None, - None, - None, - )) + ]) + .keywords(vec![name]) + .build(), + ) }) .collect(); - items.sort_unstable_by_key(|i| i.keywords.first().unwrap().clone()); + items.sort_unstable_by_key(|i| i.get_keywords().first().unwrap().clone()); Ok(Some(items)) }, None, diff --git a/src/components/list/mod.rs b/src/components/list/mod.rs index 809c6d7..02d5196 100644 --- a/src/components/list/mod.rs +++ b/src/components/list/mod.rs @@ -93,8 +93,8 @@ impl ListItem { } } -impl Render for ListItem { - fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { +impl ItemComponent for ListItem { + fn render(&self, selected: bool, cx: &WindowContext) -> AnyElement { let theme = cx.global::(); let el = if let Some(img) = &self.img { div().child(div().mr_4().child(img.clone())) @@ -128,43 +128,139 @@ impl Render for ListItem { .ml_auto() .children(self.accessories.clone()), ) + .into_any_element() + } + fn clone_box(&self) -> Box { + Box::new(self.clone()) } } -#[derive(IntoElement, Clone)] -#[allow(dead_code)] -pub struct Item { - id: u64, - pub keywords: Vec, - component: AnyView, - pub preview: Option<(f32, Box)>, - actions: Vec, - pub weight: Option, - selected: bool, - pub meta: Box, - render: Option AnyElement>, +pub trait ItemComponent { + fn render(&self, selected: bool, cx: &WindowContext) -> AnyElement; + fn clone_box(&self) -> Box; +} + +impl Clone for Box { + fn clone(&self) -> Box { + self.clone_box() + } } pub trait Meta: std::any::Any { - fn clone_box(&self) -> Box; fn value(&self) -> &dyn std::any::Any; + fn clone_meta(&self) -> Box; } impl Meta for F where F: Clone + std::any::Any, { - fn clone_box(&self) -> Box { - Box::new(self.clone()) - } fn value(&self) -> &dyn std::any::Any { self } + fn clone_meta(&self) -> Box { + Box::new(self.clone()) + } } impl<'a> Clone for Box { fn clone(&self) -> Self { - (**self).clone_box() + (**self).clone_meta() + } +} + +pub struct ItemBuilder { + id: u64, + preview: Option<(f32, Box)>, + actions: Vec, + weight: Option, + keywords: Vec, + meta: Box, + component: Box, + preset: ItemPreset, +} + +impl ItemBuilder { + pub fn new(id: impl Hash, component: impl ItemComponent + 'static) -> Self { + let mut s = DefaultHasher::new(); + id.hash(&mut s); + let id = s.finish(); + Self { + id, + preview: None, + actions: vec![], + weight: None, + keywords: vec![], + meta: Box::new(()), + preset: ItemPreset::Default, + component: Box::new(component), + } + } + pub fn preview(&mut self, width: f32, preview: impl Preview + 'static) -> &mut Self { + self.preview = Some((width, Box::new(preview))); + self + } + pub fn meta(&mut self, meta: impl Meta + 'static) -> &mut Self { + self.meta = Box::new(meta); + self + } + pub fn keywords(&mut self, keywords: Vec) -> &mut Self { + self.keywords = keywords.into_iter().map(|k| k.to_string()).collect(); + self + } + pub fn actions(&mut self, actions: Vec) -> &mut Self { + self.actions = actions; + self + } + pub fn weight(&mut self, weight: u16) -> &mut Self { + self.weight = Some(weight); + self + } + pub fn preset(&mut self, preset: ItemPreset) -> &mut Self { + self.preset = preset; + self + } + pub fn build(&self) -> Item { + Item { + id: self.id, + preview: self.preview.clone(), + actions: self.actions.clone(), + weight: self.weight, + keywords: self.keywords.clone(), + selected: false, + component: self.component.clone(), + meta: self.meta.clone_meta(), + preset: self.preset.clone(), + } + } +} + +#[derive(Clone)] +pub enum ItemPreset { + Plain, + Default, +} + +#[derive(IntoElement, Clone)] +#[allow(dead_code)] +pub struct Item { + id: u64, + preview: Option<(f32, Box)>, + actions: Vec, + weight: Option, + keywords: Vec, + meta: Box, + component: Box, + selected: bool, + preset: ItemPreset, +} + +impl Item { + pub fn get_meta(&self) -> V { + self.meta.value().downcast_ref::().cloned().unwrap() + } + pub fn get_keywords(&self) -> &Vec { + self.keywords.as_ref() } } @@ -192,52 +288,25 @@ impl<'a> Clone for Box { } } -impl Item { - pub fn new( - t: T, - keywords: Vec, - component: AnyView, - preview: Option<(f32, Box)>, - actions: Vec, - weight: Option, - meta: Option>, - render: Option AnyElement>, - ) -> Self { - let mut s = DefaultHasher::new(); - t.hash(&mut s); - let id = s.finish(); - Self { - id, - keywords: keywords.into_iter().map(|s| s.to_string()).collect(), - component, - preview, - actions, - weight, - selected: false, - meta: meta.unwrap_or_else(|| Box::new(())), - render, - } - } -} - impl RenderOnce for Item { fn render(self, cx: &mut WindowContext) -> impl IntoElement { - if let Some(render) = &self.render { - render(&self, self.selected, cx) - } else { - let theme = cx.global::(); - let mut bg_hover = theme.mantle; - bg_hover.fade_out(0.5); - if self.selected { - div().border_color(theme.crust).bg(theme.mantle) - } else { - div().hover(|s| s.bg(bg_hover)) + match self.preset { + ItemPreset::Plain => self.component.render(self.selected, cx), + ItemPreset::Default => { + let theme = cx.global::(); + let mut bg_hover = theme.mantle; + bg_hover.fade_out(0.5); + if self.selected { + div().border_color(theme.crust).bg(theme.mantle) + } else { + div().hover(|s| s.bg(bg_hover)) + } + .p_2() + .border_1() + .rounded_xl() + .child(self.component.render(self.selected, cx)) + .into_any_element() } - .p_2() - .border_1() - .rounded_xl() - .child(self.component) - .into_any_element() } } } diff --git a/src/state.rs b/src/state.rs index 97c41c0..d6f0899 100644 --- a/src/state.rs +++ b/src/state.rs @@ -8,7 +8,7 @@ use std::time::{self, Duration}; use crate::{ commands::root::list::RootListBuilder, components::{ - list::{Accessory, Item, List, ListBuilder, ListItem}, + list::{Accessory, ItemBuilder, List, ListBuilder, ListItem}, shared::{Icon, Img, ImgMask, ImgSize, ImgSource}, }, query::{TextEvent, TextInput, TextInputWeak}, @@ -936,24 +936,19 @@ impl ActionsModel { } else { vec![] }; - Some(Item::new( - item.label.clone(), - vec![item.label.clone()], - cx.new_view(|_| { + Some( + ItemBuilder::new( + item.label.clone(), ListItem::new( Some(action.image.clone()), item.label, None, accessories, - ) - }) - .into(), - None, - vec![action], - None, - None, - None, - )) + ), + ) + .actions(vec![action]) + .build(), + ) }) .collect(), )) diff --git a/src/window.rs b/src/window.rs index ab09205..a3af7cf 100644 --- a/src/window.rs +++ b/src/window.rs @@ -85,15 +85,13 @@ impl Window { .detach(); } pub async fn wait_for_close(cx: &mut AsyncWindowContext) { - loop { - while let Ok(active) = - cx.update_window::(cx.window_handle(), |_, cx| cx.is_window_active()) - { - if !active { - break; - } - sleep(Duration::from_millis(10)).await; + while let Ok(active) = + cx.update_window::(cx.window_handle(), |_, cx| cx.is_window_active()) + { + if !active { + break; } + sleep(Duration::from_millis(10)).await; } } }