diff --git a/Cargo.lock b/Cargo.lock index a018d39..39e77f8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1014,7 +1014,7 @@ dependencies = [ [[package]] name = "collections" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#b7c6ffa6c2598dceb4bb4804538e636ed8e85700" +source = "git+https://github.com/zed-industries/zed#a6b1514246c2efeefde5ed0f1fb18aac5c7cc8b2" dependencies = [ "indexmap", "rustc-hash 2.1.0", @@ -1213,9 +1213,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" dependencies = [ "libc", ] @@ -1318,7 +1318,7 @@ dependencies = [ [[package]] name = "derive_refineable" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#b7c6ffa6c2598dceb4bb4804538e636ed8e85700" +source = "git+https://github.com/zed-industries/zed#a6b1514246c2efeefde5ed0f1fb18aac5c7cc8b2" dependencies = [ "proc-macro2", "quote", @@ -2013,7 +2013,7 @@ dependencies = [ [[package]] name = "gpui" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#b7c6ffa6c2598dceb4bb4804538e636ed8e85700" +source = "git+https://github.com/zed-industries/zed#a6b1514246c2efeefde5ed0f1fb18aac5c7cc8b2" dependencies = [ "anyhow", "as-raw-xcb-connection", @@ -2098,7 +2098,7 @@ dependencies = [ [[package]] name = "gpui_macros" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#b7c6ffa6c2598dceb4bb4804538e636ed8e85700" +source = "git+https://github.com/zed-industries/zed#a6b1514246c2efeefde5ed0f1fb18aac5c7cc8b2" dependencies = [ "proc-macro2", "quote", @@ -2303,7 +2303,7 @@ dependencies = [ [[package]] name = "http_client" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#b7c6ffa6c2598dceb4bb4804538e636ed8e85700" +source = "git+https://github.com/zed-industries/zed#a6b1514246c2efeefde5ed0f1fb18aac5c7cc8b2" dependencies = [ "anyhow", "bytes", @@ -2913,7 +2913,7 @@ dependencies = [ [[package]] name = "media" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#b7c6ffa6c2598dceb4bb4804538e636ed8e85700" +source = "git+https://github.com/zed-industries/zed#a6b1514246c2efeefde5ed0f1fb18aac5c7cc8b2" dependencies = [ "anyhow", "bindgen", @@ -3051,8 +3051,7 @@ checksum = "e664971378a3987224f7a0e10059782035e89899ae403718ee07de85bec42afe" [[package]] name = "negentropy" version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a88da9dd148bbcdce323dd6ac47d369b4769d4a3b78c6c52389b9269f77932" +source = "git+https://github.com/rust-nostr/negentropy?rev=311013ce05dd3f670d9d9c444c09195837837271#311013ce05dd3f670d9d9c444c09195837837271" [[package]] name = "new_debug_unreachable" @@ -3092,7 +3091,7 @@ checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" [[package]] name = "nostr" version = "0.38.0" -source = "git+https://github.com/rust-nostr/nostr#1470b8b00437e586fb86035484f942d6202db83a" +source = "git+https://github.com/rust-nostr/nostr#d8dcb0697b1ebce28cce6f89747dc6ba73a45b56" dependencies = [ "aes", "base64", @@ -3120,7 +3119,7 @@ dependencies = [ [[package]] name = "nostr-database" version = "0.38.0" -source = "git+https://github.com/rust-nostr/nostr#1470b8b00437e586fb86035484f942d6202db83a" +source = "git+https://github.com/rust-nostr/nostr#d8dcb0697b1ebce28cce6f89747dc6ba73a45b56" dependencies = [ "flatbuffers", "nostr", @@ -3130,7 +3129,7 @@ dependencies = [ [[package]] name = "nostr-lmdb" version = "0.38.0" -source = "git+https://github.com/rust-nostr/nostr#1470b8b00437e586fb86035484f942d6202db83a" +source = "git+https://github.com/rust-nostr/nostr#d8dcb0697b1ebce28cce6f89747dc6ba73a45b56" dependencies = [ "async-utility", "heed", @@ -3141,7 +3140,7 @@ dependencies = [ [[package]] name = "nostr-relay-pool" version = "0.38.0" -source = "git+https://github.com/rust-nostr/nostr#1470b8b00437e586fb86035484f942d6202db83a" +source = "git+https://github.com/rust-nostr/nostr#d8dcb0697b1ebce28cce6f89747dc6ba73a45b56" dependencies = [ "async-utility", "async-wsocket", @@ -3157,7 +3156,7 @@ dependencies = [ [[package]] name = "nostr-sdk" version = "0.38.0" -source = "git+https://github.com/rust-nostr/nostr#1470b8b00437e586fb86035484f942d6202db83a" +source = "git+https://github.com/rust-nostr/nostr#d8dcb0697b1ebce28cce6f89747dc6ba73a45b56" dependencies = [ "async-utility", "nostr", @@ -4164,7 +4163,7 @@ dependencies = [ [[package]] name = "refineable" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#b7c6ffa6c2598dceb4bb4804538e636ed8e85700" +source = "git+https://github.com/zed-industries/zed#a6b1514246c2efeefde5ed0f1fb18aac5c7cc8b2" dependencies = [ "derive_refineable", ] @@ -4305,7 +4304,7 @@ dependencies = [ [[package]] name = "reqwest_client" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#b7c6ffa6c2598dceb4bb4804538e636ed8e85700" +source = "git+https://github.com/zed-industries/zed#a6b1514246c2efeefde5ed0f1fb18aac5c7cc8b2" dependencies = [ "anyhow", "bytes", @@ -4655,7 +4654,7 @@ checksum = "c2fdfc24bc566f839a2da4c4295b82db7d25a24253867d5c64355abb5799bdbe" [[package]] name = "semantic_version" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#b7c6ffa6c2598dceb4bb4804538e636ed8e85700" +source = "git+https://github.com/zed-industries/zed#a6b1514246c2efeefde5ed0f1fb18aac5c7cc8b2" dependencies = [ "anyhow", "serde", @@ -4980,7 +4979,7 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "sum_tree" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#b7c6ffa6c2598dceb4bb4804538e636ed8e85700" +source = "git+https://github.com/zed-industries/zed#a6b1514246c2efeefde5ed0f1fb18aac5c7cc8b2" dependencies = [ "arrayvec", "log", @@ -5802,7 +5801,7 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "util" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#b7c6ffa6c2598dceb4bb4804538e636ed8e85700" +source = "git+https://github.com/zed-industries/zed#a6b1514246c2efeefde5ed0f1fb18aac5c7cc8b2" dependencies = [ "anyhow", "async-fs", diff --git a/crates/app/src/main.rs b/crates/app/src/main.rs index 52f44d8..b552214 100644 --- a/crates/app/src/main.rs +++ b/crates/app/src/main.rs @@ -4,8 +4,8 @@ use common::constants::{ NEW_MESSAGE_SUB_ID, }; use gpui::{ - actions, point, px, size, App, AppContext, Bounds, SharedString, TitlebarOptions, - VisualContext, WindowBounds, WindowKind, WindowOptions, + actions, point, px, size, App, AppContext, Application, Bounds, SharedString, TitlebarOptions, + WindowBounds, WindowKind, WindowOptions, }; #[cfg(target_os = "linux")] use gpui::{WindowBackgroundAppearance, WindowDecorations}; @@ -67,7 +67,7 @@ async fn main() { // Merge all requests into single subscription tokio::spawn(async move { handle_metadata(client, mta_rx).await }); - App::new() + Application::new() .with_assets(Assets) .with_http_client(Arc::new(reqwest_client::ReqwestClient::new())) .run(move |cx| { @@ -177,12 +177,11 @@ async fn main() { ..Default::default() }; - cx.open_window(opts, |cx| { - cx.set_window_title(APP_NAME); - cx.set_app_id(APP_ID); + cx.open_window(opts, |window, cx| { + window.set_window_title(APP_NAME); + window.set_app_id(APP_ID); cx.activate(true); - - cx.new_view(|cx| Root::new(cx.new_view(AppView::new).into(), cx)) + cx.new(|cx| Root::new(cx.new(|cx| AppView::new(window, cx)).into(), window, cx)) }) .expect("System error"); }); @@ -301,6 +300,6 @@ async fn handle_metadata(client: &'static Client, mut mta_rx: mpsc::Receiver, - dock: View, + onboarding: Entity, + dock: Entity, } impl AppView { - pub fn new(cx: &mut ViewContext<'_, Self>) -> AppView { - let onboarding = cx.new_view(Onboarding::new); - let dock = cx.new_view(|cx| DockArea::new(DOCK_AREA.id, Some(DOCK_AREA.version), cx)); + pub fn new(window: &mut Window, cx: &mut Context<'_, Self>) -> AppView { + let onboarding = cx.new(|cx| Onboarding::new(window, cx)); + let dock = cx.new(|cx| DockArea::new(DOCK_AREA.id, Some(DOCK_AREA.version), window, cx)); // Get current user from app state let weak_user = cx.global::().user(); if let Some(user) = weak_user.upgrade() { - cx.observe(&user, move |view, this, cx| { + cx.observe_in(&user, window, |view, this, window, cx| { if this.read(cx).is_some() { - Self::render_dock(view.dock.downgrade(), cx); + Self::render_dock(view.dock.downgrade(), window, cx); } }) .detach(); @@ -69,30 +69,33 @@ impl AppView { AppView { onboarding, dock } } - fn render_dock(dock_area: WeakView, cx: &mut WindowContext) { - let left = DockItem::panel(Arc::new(Sidebar::new(cx))); + fn render_dock(dock_area: WeakEntity, window: &mut Window, cx: &mut App) { + let left = DockItem::panel(Arc::new(Sidebar::new(window, cx))); let center = DockItem::split_with_sizes( Axis::Vertical, vec![DockItem::tabs( - vec![Arc::new(WelcomePanel::new(cx))], + vec![Arc::new(WelcomePanel::new(window, cx))], None, &dock_area, + window, cx, )], vec![None], &dock_area, + window, cx, ); _ = dock_area.update(cx, |view, cx| { - view.set_version(DOCK_AREA.version, cx); - view.set_left_dock(left, Some(px(240.)), true, cx); - view.set_center(center, cx); + view.set_version(DOCK_AREA.version, window, cx); + view.set_left_dock(left, Some(px(240.)), true, window, cx); + view.set_center(center, window, cx); view.set_dock_collapsible( Edges { left: false, ..Default::default() }, + window, cx, ); // TODO: support right dock? @@ -112,7 +115,7 @@ impl AppView { .rounded_full() .object_fit(ObjectFit::Cover), ) - .popup_menu(move |this, _cx| { + .popup_menu(move |this, _, _cx| { this.menu("Profile", Box::new(OpenProfile)) .menu("Contacts", Box::new(OpenContacts)) .menu("Settings", Box::new(OpenSettings)) @@ -121,40 +124,58 @@ impl AppView { }) } - fn on_panel_action(&mut self, action: &AddPanel, cx: &mut ViewContext) { + fn on_panel_action(&mut self, action: &AddPanel, window: &mut Window, cx: &mut Context) { match &action.panel { PanelKind::Room(id) => { if let Some(weak_room) = cx.global::().room(id, cx) { if let Some(room) = weak_room.upgrade() { - let panel = Arc::new(ChatPanel::new(room, cx)); + let panel = Arc::new(ChatPanel::new(room, window, cx)); self.dock.update(cx, |dock_area, cx| { - dock_area.add_panel(panel, action.position, cx); + dock_area.add_panel(panel, action.position, window, cx); }); } else { - cx.push_notification(( - NotificationType::Error, - "System error. Cannot open this chat room.", - )); + window.push_notification( + ( + NotificationType::Error, + "System error. Cannot open this chat room.", + ), + cx, + ); } } } }; } - fn on_profile_action(&mut self, _action: &OpenProfile, cx: &mut ViewContext) { + fn on_profile_action( + &mut self, + _action: &OpenProfile, + window: &mut Window, + cx: &mut Context, + ) { // TODO } - fn on_contacts_action(&mut self, _action: &OpenContacts, cx: &mut ViewContext) { + fn on_contacts_action( + &mut self, + _action: &OpenContacts, + window: &mut Window, + cx: &mut Context, + ) { // TODO } - fn on_settings_action(&mut self, _action: &OpenSettings, cx: &mut ViewContext) { + fn on_settings_action( + &mut self, + _action: &OpenSettings, + window: &mut Window, + cx: &mut Context, + ) { // TODO } - fn on_logout_action(&mut self, _action: &Logout, cx: &mut ViewContext) { + fn on_logout_action(&mut self, _action: &Logout, window: &mut Window, cx: &mut Context) { cx.update_global::(|this, cx| { this.logout(cx); // Reset nostr client @@ -166,9 +187,9 @@ impl AppView { } impl Render for AppView { - fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - let modal_layer = Root::render_modal_layer(cx); - let notification_layer = Root::render_notification_layer(cx); + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let modal_layer = Root::render_modal_layer(window, cx); + let notification_layer = Root::render_notification_layer(window, cx); let state = cx.global::(); div() @@ -189,7 +210,7 @@ impl Render for AppView { .text_color(cx.theme().base.step(cx, ColorScaleStep::THREE)), ), ) - } else if let Some(contact) = state.current_user(cx) { + } else if let Some(contact) = state.current_user(window, cx) { this.child( TitleBar::new() // Left side diff --git a/crates/app/src/views/chat/message.rs b/crates/app/src/views/chat/message.rs index b0c03ef..9d6ac8a 100644 --- a/crates/app/src/views/chat/message.rs +++ b/crates/app/src/views/chat/message.rs @@ -1,6 +1,6 @@ use gpui::{ - div, img, px, InteractiveElement, IntoElement, ParentElement, RenderOnce, SharedString, Styled, - WindowContext, + div, img, px, App, InteractiveElement, IntoElement, ParentElement, RenderOnce, SharedString, + Styled, Window, }; use registry::contact::Contact; use ui::{ @@ -36,7 +36,7 @@ impl Message { } impl RenderOnce for Message { - fn render(self, cx: &mut WindowContext) -> impl IntoElement { + fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { div() .group(&self.ago) .relative() diff --git a/crates/app/src/views/chat/mod.rs b/crates/app/src/views/chat/mod.rs index 3c6376d..53d3c0d 100644 --- a/crates/app/src/views/chat/mod.rs +++ b/crates/app/src/views/chat/mod.rs @@ -4,10 +4,10 @@ use common::{ utils::{compare, message_time, nip96_upload}, }; use gpui::{ - div, img, list, px, white, AnyElement, AppContext, Context, EventEmitter, Flatten, FocusHandle, - FocusableView, InteractiveElement, IntoElement, ListAlignment, ListState, Model, ObjectFit, + div, img, list, px, white, AnyElement, App, AppContext, Context, Entity, EventEmitter, Flatten, + FocusHandle, Focusable, InteractiveElement, IntoElement, ListAlignment, ListState, ObjectFit, ParentElement, PathPromptOptions, Pixels, Render, SharedString, StatefulInteractiveElement, - Styled, StyledImage, View, ViewContext, VisualContext, WeakModel, WeakView, WindowContext, + Styled, StyledImage, WeakEntity, Window, }; use itertools::Itertools; use message::Message; @@ -18,10 +18,7 @@ use state::get_client; use tokio::sync::oneshot; use ui::{ button::{Button, ButtonRounded, ButtonVariants}, - dock_area::{ - panel::{Panel, PanelEvent}, - state::PanelState, - }, + dock_area::panel::{Panel, PanelEvent}, input::{InputEvent, TextInput}, popup_menu::PopupMenu, prelude::FluentBuilder, @@ -45,48 +42,51 @@ pub struct ChatPanel { // Chat Room id: SharedString, name: SharedString, - room: Model, - state: Model, + room: Entity, + state: Entity, list: ListState, // New Message - input: View, + input: Entity, // Media - attaches: Model>>, + attaches: Entity>>, is_uploading: bool, } impl ChatPanel { - pub fn new(model: Model, cx: &mut WindowContext) -> View { + pub fn new(model: Entity, window: &mut Window, cx: &mut App) -> Entity { let room = model.read(cx); let id = room.id.to_string().into(); let name = room.title.clone().unwrap_or("Untitled".into()); - cx.observe_new_views::(|this, cx| { - this.load_messages(cx); - }) - .detach(); + cx.new(|cx| { + cx.observe_new::(|this, window, cx| { + if let Some(window) = window { + this.load_messages(window, cx); + } + }) + .detach(); - cx.new_view(|cx| { // Form - let input = cx.new_view(|cx| { - TextInput::new(cx) + let input = cx.new(|cx| { + TextInput::new(window, cx) .appearance(false) .text_size(ui::Size::Small) .placeholder("Message...") }); // List - let state = cx.new_model(|_| State { + let state = cx.new(|_| State { count: 0, items: vec![], }); // Send message when user presses enter - cx.subscribe( + cx.subscribe_in( &input, - move |this: &mut ChatPanel, view, input_event, cx| { + window, + move |this: &mut ChatPanel, view, input_event, window, cx| { if let InputEvent::PressEnter = input_event { - this.send_message(view.downgrade(), cx); + this.send_message(view.downgrade(), window, cx); } }, ) @@ -100,7 +100,7 @@ impl ChatPanel { items.len(), ListAlignment::Bottom, Pixels(256.), - move |idx, _cx| { + move |idx, _window, _cx| { let item = items.get(idx).unwrap().clone(); div().child(item).into_any_element() }, @@ -110,19 +110,19 @@ impl ChatPanel { }) .detach(); - cx.observe(&model, |this, model, cx| { - this.load_new_messages(model.downgrade(), cx); + cx.observe_in(&model, window, |this, model, window, cx| { + this.load_new_messages(model.downgrade(), window, cx); }) .detach(); - let attaches = cx.new_model(|_| None); + let attaches = cx.new(|_| None); Self { closeable: true, zoomable: true, focus_handle: cx.focus_handle(), room: model, - list: ListState::new(0, ListAlignment::Bottom, Pixels(256.), move |_, _| { + list: ListState::new(0, ListAlignment::Bottom, Pixels(256.), move |_, _, _| { div().into_any_element() }), is_uploading: false, @@ -135,7 +135,7 @@ impl ChatPanel { }) } - fn load_messages(&self, cx: &mut ViewContext) { + fn load_messages(&self, window: &mut Window, cx: &mut Context) { let room = self.room.read(cx); let members = room.members.clone(); let owner = room.owner.clone(); @@ -206,7 +206,7 @@ impl ChatPanel { let total = items.len(); - _ = async_cx.update_model(&async_state, |a, b| { + _ = async_cx.update_entity(&async_state, |a, b| { a.items = items; a.count = total; b.notify(); @@ -216,7 +216,12 @@ impl ChatPanel { .detach(); } - fn load_new_messages(&self, model: WeakModel, cx: &mut ViewContext) { + fn load_new_messages( + &self, + model: WeakEntity, + window: &mut Window, + cx: &mut Context, + ) { if let Some(model) = model.upgrade() { let room = model.read(cx); let items: Vec = room @@ -233,7 +238,7 @@ impl ChatPanel { }) .collect(); - cx.update_model(&self.state, |model, cx| { + cx.update_entity(&self.state, |model, cx| { let messages: Vec = items .into_iter() .filter_map(|new| { @@ -252,7 +257,12 @@ impl ChatPanel { } } - fn send_message(&mut self, view: WeakView, cx: &mut ViewContext) { + fn send_message( + &mut self, + view: WeakEntity, + window: &mut Window, + cx: &mut Context, + ) { let room = self.room.read(cx); let owner = room.owner.clone(); let mut members = room.members.to_vec(); @@ -262,7 +272,7 @@ impl ChatPanel { let mut content = self.input.read(cx).text().to_string(); if content.is_empty() { - cx.push_notification("Message cannot be empty"); + window.push_notification("Message cannot be empty", cx); return; } @@ -279,12 +289,13 @@ impl ChatPanel { // Update input state if let Some(input) = view.upgrade() { - cx.update_view(&input, |input, cx| { - input.set_loading(true, cx); - input.set_disabled(true, cx); + cx.update_entity(&input, |input, cx| { + input.set_loading(true, window, cx); + input.set_disabled(true, window, cx); }); } + /* cx.spawn(|this, mut async_cx| async move { // Send message to all members async_cx @@ -315,8 +326,8 @@ impl ChatPanel { .detach(); if let Some(view) = this.upgrade() { - _ = async_cx.update_view(&view, |this, cx| { - cx.update_model(&this.state, |model, cx| { + _ = async_cx.update_entity(&view, |this, cx| { + cx.update_entity(&this.state, |model, cx| { let message = Message::new( owner, content.to_string().into(), @@ -331,17 +342,18 @@ impl ChatPanel { } if let Some(input) = view.upgrade() { - _ = async_cx.update_view(&input, |input, cx| { - input.set_loading(false, cx); - input.set_disabled(false, cx); - input.set_text("", cx); + _ = async_cx.update_entity(&input, |input, cx| { + input.set_loading(false, window, cx); + input.set_disabled(false, window, cx); + input.set_text("", window, cx); }); } }) .detach(); + */ } - fn upload(&mut self, cx: &mut ViewContext) { + fn upload(&mut self, window: &mut Window, cx: &mut Context) { let attaches = self.attaches.clone(); let paths = cx.prompt_for_paths(PathPromptOptions { files: true, @@ -373,14 +385,14 @@ impl ChatPanel { if let Ok(url) = rx.await { // Stop loading spinner if let Some(view) = this.upgrade() { - _ = async_cx.update_view(&view, |this, cx| { + _ = async_cx.update_entity(&view, |this, cx| { this.is_uploading = false; cx.notify(); }); } // Update attaches model - _ = async_cx.update_model(&attaches, |model, cx| { + _ = async_cx.update_entity(&attaches, |model, cx| { if let Some(model) = model.as_mut() { model.push(url); } else { @@ -398,7 +410,7 @@ impl ChatPanel { .detach(); } - fn remove(&mut self, url: &Url, cx: &mut ViewContext) { + fn remove(&mut self, url: &Url, window: &mut Window, cx: &mut Context) { self.attaches.update(cx, |model, cx| { if let Some(urls) = model.as_mut() { let ix = urls.iter().position(|x| x == url).unwrap(); @@ -414,7 +426,7 @@ impl Panel for ChatPanel { self.id.clone() } - fn panel_facepile(&self, cx: &WindowContext) -> Option> { + fn panel_facepile(&self, cx: &App) -> Option> { Some( self.room .read(cx) @@ -425,41 +437,37 @@ impl Panel for ChatPanel { ) } - fn title(&self, _cx: &WindowContext) -> AnyElement { + fn title(&self, _cx: &App) -> AnyElement { self.name.clone().into_any_element() } - fn closeable(&self, _cx: &WindowContext) -> bool { + fn closeable(&self, _cx: &App) -> bool { self.closeable } - fn zoomable(&self, _cx: &WindowContext) -> bool { + fn zoomable(&self, _cx: &App) -> bool { self.zoomable } - fn popup_menu(&self, menu: PopupMenu, _cx: &WindowContext) -> PopupMenu { + fn popup_menu(&self, menu: PopupMenu, _cx: &App) -> PopupMenu { menu.track_focus(&self.focus_handle) } - fn toolbar_buttons(&self, _cx: &WindowContext) -> Vec