1use std::time::Duration;
2
3use dev_server_projects::{DevServer, DevServerId, DevServerProject, DevServerProjectId};
4use editor::Editor;
5use feature_flags::FeatureFlagViewExt;
6use gpui::{
7 percentage, Action, Animation, AnimationExt, AnyElement, AppContext, ClipboardItem,
8 DismissEvent, EventEmitter, FocusHandle, FocusableView, Model, ScrollHandle, Transformation,
9 View, ViewContext,
10};
11use rpc::{
12 proto::{CreateDevServerResponse, DevServerStatus},
13 ErrorCode, ErrorExt,
14};
15use settings::Settings;
16use theme::ThemeSettings;
17use ui::{prelude::*, Indicator, List, ListHeader, ListItem, ModalContent, ModalHeader, Tooltip};
18use ui_text_field::{FieldLabelLayout, TextField};
19use util::ResultExt;
20use workspace::{notifications::DetachAndPromptErr, AppState, ModalView, Workspace, WORKSPACE_DB};
21
22use crate::OpenRemote;
23
24pub struct DevServerProjects {
25 mode: Mode,
26 focus_handle: FocusHandle,
27 scroll_handle: ScrollHandle,
28 dev_server_store: Model<dev_server_projects::Store>,
29 project_path_input: View<Editor>,
30 dev_server_name_input: View<TextField>,
31 _subscription: gpui::Subscription,
32}
33
34#[derive(Default)]
35struct CreateDevServer {
36 creating: bool,
37 dev_server: Option<CreateDevServerResponse>,
38}
39
40#[derive(Clone)]
41struct CreateDevServerProject {
42 dev_server_id: DevServerId,
43 creating: bool,
44}
45
46enum Mode {
47 Default(Option<CreateDevServerProject>),
48 CreateDevServer(CreateDevServer),
49}
50
51impl DevServerProjects {
52 pub fn register(_: &mut Workspace, cx: &mut ViewContext<Workspace>) {
53 cx.observe_flag::<feature_flags::Remoting, _>(|enabled, workspace, _| {
54 if enabled {
55 workspace.register_action(|workspace, _: &OpenRemote, cx| {
56 workspace.toggle_modal(cx, |cx| Self::new(cx))
57 });
58 }
59 })
60 .detach();
61 }
62
63 pub fn open(workspace: View<Workspace>, cx: &mut WindowContext) {
64 workspace.update(cx, |workspace, cx| {
65 workspace.toggle_modal(cx, |cx| Self::new(cx))
66 })
67 }
68
69 pub fn new(cx: &mut ViewContext<Self>) -> Self {
70 let project_path_input = cx.new_view(|cx| {
71 let mut editor = Editor::single_line(cx);
72 editor.set_placeholder_text("Project path", cx);
73 editor
74 });
75 let dev_server_name_input =
76 cx.new_view(|cx| TextField::new(cx, "Name", "").with_label(FieldLabelLayout::Stacked));
77
78 let focus_handle = cx.focus_handle();
79 let dev_server_store = dev_server_projects::Store::global(cx);
80
81 let subscription = cx.observe(&dev_server_store, |_, _, cx| {
82 cx.notify();
83 });
84
85 Self {
86 mode: Mode::Default(None),
87 focus_handle,
88 scroll_handle: ScrollHandle::new(),
89 dev_server_store,
90 project_path_input,
91 dev_server_name_input,
92 _subscription: subscription,
93 }
94 }
95
96 pub fn create_dev_server_project(
97 &mut self,
98 dev_server_id: DevServerId,
99 cx: &mut ViewContext<Self>,
100 ) {
101 let path = self.project_path_input.read(cx).text(cx).trim().to_string();
102
103 if path == "" {
104 return;
105 }
106
107 if self
108 .dev_server_store
109 .read(cx)
110 .projects_for_server(dev_server_id)
111 .iter()
112 .any(|p| p.path == path)
113 {
114 cx.spawn(|_, mut cx| async move {
115 cx.prompt(
116 gpui::PromptLevel::Critical,
117 "Failed to create project",
118 Some(&format!(
119 "Project {} already exists for this dev server.",
120 path
121 )),
122 &["Ok"],
123 )
124 .await
125 })
126 .detach_and_log_err(cx);
127 return;
128 }
129
130 let create = {
131 let path = path.clone();
132 self.dev_server_store.update(cx, |store, cx| {
133 store.create_dev_server_project(dev_server_id, path, cx)
134 })
135 };
136
137 cx.spawn(|this, mut cx| async move {
138 let result = create.await;
139 this.update(&mut cx, |this, cx| {
140 if result.is_ok() {
141 this.project_path_input.update(cx, |editor, cx| {
142 editor.set_text("", cx);
143 });
144 this.mode = Mode::Default(None);
145 } else {
146 this.mode = Mode::Default(Some(CreateDevServerProject {
147 dev_server_id,
148 creating: false,
149 }));
150 }
151 })
152 .log_err();
153 result
154 })
155 .detach_and_prompt_err("Failed to create project", cx, move |e, _| {
156 match e.error_code() {
157 ErrorCode::DevServerOffline => Some(
158 "The dev server is offline. Please log in and check it is connected."
159 .to_string(),
160 ),
161 ErrorCode::DevServerProjectPathDoesNotExist => {
162 Some(format!("The path `{}` does not exist on the server.", path))
163 }
164 _ => None,
165 }
166 });
167
168 self.mode = Mode::Default(Some(CreateDevServerProject {
169 dev_server_id,
170 creating: true,
171 }));
172 }
173
174 pub fn create_dev_server(&mut self, cx: &mut ViewContext<Self>) {
175 let name = self
176 .dev_server_name_input
177 .read(cx)
178 .editor()
179 .read(cx)
180 .text(cx)
181 .trim()
182 .to_string();
183
184 if name == "" {
185 return;
186 }
187
188 let dev_server = self
189 .dev_server_store
190 .update(cx, |store, cx| store.create_dev_server(name.clone(), cx));
191
192 cx.spawn(|this, mut cx| async move {
193 let result = dev_server.await;
194
195 this.update(&mut cx, |this, _| match &result {
196 Ok(dev_server) => {
197 this.mode = Mode::CreateDevServer(CreateDevServer {
198 creating: false,
199 dev_server: Some(dev_server.clone()),
200 });
201 }
202 Err(_) => {
203 this.mode = Mode::CreateDevServer(Default::default());
204 }
205 })
206 .log_err();
207 result
208 })
209 .detach_and_prompt_err("Failed to create server", cx, |_, _| None);
210
211 self.mode = Mode::CreateDevServer(CreateDevServer {
212 creating: true,
213 dev_server: None,
214 });
215 cx.notify()
216 }
217
218 fn delete_dev_server(&mut self, id: DevServerId, cx: &mut ViewContext<Self>) {
219 let answer = cx.prompt(
220 gpui::PromptLevel::Destructive,
221 "Are you sure?",
222 Some("This will delete the dev server and all of its remote projects."),
223 &["Delete", "Cancel"],
224 );
225
226 cx.spawn(|this, mut cx| async move {
227 let answer = answer.await?;
228
229 if answer != 0 {
230 return Ok(());
231 }
232
233 let project_ids: Vec<DevServerProjectId> = this.update(&mut cx, |this, cx| {
234 this.dev_server_store.update(cx, |store, _| {
235 store
236 .projects_for_server(id)
237 .into_iter()
238 .map(|project| project.id)
239 .collect()
240 })
241 })?;
242
243 this.update(&mut cx, |this, cx| {
244 this.dev_server_store
245 .update(cx, |store, cx| store.delete_dev_server(id, cx))
246 })?
247 .await?;
248
249 for id in project_ids {
250 WORKSPACE_DB
251 .delete_workspace_by_dev_server_project_id(id)
252 .await
253 .log_err();
254 }
255 Ok(())
256 })
257 .detach_and_prompt_err("Failed to delete dev server", cx, |_, _| None);
258 }
259
260 fn delete_dev_server_project(
261 &mut self,
262 id: DevServerProjectId,
263 path: &str,
264 cx: &mut ViewContext<Self>,
265 ) {
266 let answer = cx.prompt(
267 gpui::PromptLevel::Destructive,
268 format!("Delete \"{}\"?", path).as_str(),
269 Some("This will delete the remote project. You can always re-add it later."),
270 &["Delete", "Cancel"],
271 );
272
273 cx.spawn(|this, mut cx| async move {
274 let answer = answer.await?;
275
276 if answer != 0 {
277 return Ok(());
278 }
279
280 this.update(&mut cx, |this, cx| {
281 this.dev_server_store
282 .update(cx, |store, cx| store.delete_dev_server_project(id, cx))
283 })?
284 .await?;
285
286 WORKSPACE_DB
287 .delete_workspace_by_dev_server_project_id(id)
288 .await
289 .log_err();
290
291 Ok(())
292 })
293 .detach_and_prompt_err("Failed to delete dev server project", cx, |_, _| None);
294 }
295
296 fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
297 match &self.mode {
298 Mode::Default(None) => {}
299 Mode::Default(Some(create_project)) => {
300 self.create_dev_server_project(create_project.dev_server_id, cx);
301 }
302 Mode::CreateDevServer(_) => {
303 self.create_dev_server(cx);
304 }
305 }
306 }
307
308 fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
309 match self.mode {
310 Mode::Default(None) => cx.emit(DismissEvent),
311 _ => {
312 self.mode = Mode::Default(None);
313 self.focus_handle(cx).focus(cx);
314 cx.notify();
315 }
316 }
317 }
318
319 fn render_dev_server(
320 &mut self,
321 dev_server: &DevServer,
322 mut create_project: Option<CreateDevServerProject>,
323 cx: &mut ViewContext<Self>,
324 ) -> impl IntoElement {
325 let dev_server_id = dev_server.id;
326 let status = dev_server.status;
327 if create_project
328 .as_ref()
329 .is_some_and(|cp| cp.dev_server_id != dev_server.id)
330 {
331 create_project = None;
332 }
333
334 v_flex()
335 .w_full()
336 .child(
337 h_flex()
338 .group("dev-server")
339 .justify_between()
340 .child(
341 h_flex()
342 .gap_2()
343 .child(
344 div()
345 .id(("status", dev_server.id.0))
346 .relative()
347 .child(Icon::new(IconName::Server).size(IconSize::Small))
348 .child(
349 div().absolute().bottom_0().left(rems_from_px(8.0)).child(
350 Indicator::dot().color(match status {
351 DevServerStatus::Online => Color::Created,
352 DevServerStatus::Offline => Color::Hidden,
353 }),
354 ),
355 )
356 .tooltip(move |cx| {
357 Tooltip::text(
358 match status {
359 DevServerStatus::Online => "Online",
360 DevServerStatus::Offline => "Offline",
361 },
362 cx,
363 )
364 }),
365 )
366 .child(dev_server.name.clone())
367 .child(
368 h_flex()
369 .visible_on_hover("dev-server")
370 .gap_1()
371 .child(
372 IconButton::new("edit-dev-server", IconName::Pencil)
373 .disabled(true) //TODO implement this on the collab side
374 .tooltip(|cx| {
375 Tooltip::text("Coming Soon - Edit dev server", cx)
376 }),
377 )
378 .child({
379 let dev_server_id = dev_server.id;
380 IconButton::new("remove-dev-server", IconName::Trash)
381 .on_click(cx.listener(move |this, _, cx| {
382 this.delete_dev_server(dev_server_id, cx)
383 }))
384 .tooltip(|cx| Tooltip::text("Remove dev server", cx))
385 }),
386 ),
387 )
388 .child(
389 h_flex().gap_1().child(
390 IconButton::new(
391 ("add-remote-project", dev_server_id.0),
392 IconName::Plus,
393 )
394 .tooltip(|cx| Tooltip::text("Add a remote project", cx))
395 .on_click(cx.listener(
396 move |this, _, cx| {
397 if let Mode::Default(project) = &mut this.mode {
398 *project = Some(CreateDevServerProject {
399 dev_server_id,
400 creating: false,
401 });
402 }
403 this.project_path_input.read(cx).focus_handle(cx).focus(cx);
404 cx.notify();
405 },
406 )),
407 ),
408 ),
409 )
410 .child(
411 v_flex()
412 .w_full()
413 .bg(cx.theme().colors().title_bar_background) // todo: this should be distinct
414 .border()
415 .border_color(cx.theme().colors().border_variant)
416 .rounded_md()
417 .my_1()
418 .py_0p5()
419 .px_3()
420 .child(
421 List::new()
422 .empty_message("No projects.")
423 .children(
424 self.dev_server_store
425 .read(cx)
426 .projects_for_server(dev_server.id)
427 .iter()
428 .map(|p| self.render_dev_server_project(p, cx)),
429 )
430 .when_some(create_project, |el, create_project| {
431 el.child(self.render_create_new_project(&create_project, cx))
432 }),
433 ),
434 )
435 }
436
437 fn render_create_new_project(
438 &mut self,
439 create_project: &CreateDevServerProject,
440 _: &mut ViewContext<Self>,
441 ) -> impl IntoElement {
442 ListItem::new("create-remote-project")
443 .start_slot(Icon::new(IconName::FileTree).color(Color::Muted))
444 .child(self.project_path_input.clone())
445 .child(
446 div()
447 .w(IconSize::Medium.rems())
448 .when(create_project.creating, |el| {
449 el.child(
450 Icon::new(IconName::ArrowCircle)
451 .size(IconSize::Medium)
452 .with_animation(
453 "arrow-circle",
454 Animation::new(Duration::from_secs(2)).repeat(),
455 |icon, delta| {
456 icon.transform(Transformation::rotate(percentage(delta)))
457 },
458 ),
459 )
460 }),
461 )
462 }
463
464 fn render_dev_server_project(
465 &mut self,
466 project: &DevServerProject,
467 cx: &mut ViewContext<Self>,
468 ) -> impl IntoElement {
469 let dev_server_project_id = project.id;
470 let project_id = project.project_id;
471 let is_online = project_id.is_some();
472 let project_path = project.path.clone();
473
474 ListItem::new(("remote-project", dev_server_project_id.0))
475 .start_slot(Icon::new(IconName::FileTree).when(!is_online, |icon| icon.color(Color::Muted)))
476 .child(
477 Label::new(project.path.clone())
478 )
479 .on_click(cx.listener(move |_, _, cx| {
480 if let Some(project_id) = project_id {
481 if let Some(app_state) = AppState::global(cx).upgrade() {
482 workspace::join_dev_server_project(project_id, app_state, None, cx)
483 .detach_and_prompt_err("Could not join project", cx, |_, _| None)
484 }
485 } else {
486 cx.spawn(|_, mut cx| async move {
487 cx.prompt(gpui::PromptLevel::Critical, "This project is offline", Some("The `zed` instance running on this dev server is not connected. You will have to restart it."), &["Ok"]).await.log_err();
488 }).detach();
489 }
490 }))
491 .end_hover_slot::<AnyElement>(Some(IconButton::new("remove-remote-project", IconName::Trash)
492 .on_click(cx.listener(move |this, _, cx| {
493 this.delete_dev_server_project(dev_server_project_id, &project_path, cx)
494 }))
495 .tooltip(|cx| Tooltip::text("Delete remote project", cx)).into_any_element()))
496 }
497
498 fn render_create_dev_server(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
499 let Mode::CreateDevServer(CreateDevServer {
500 creating,
501 dev_server,
502 }) = &self.mode
503 else {
504 unreachable!()
505 };
506
507 self.dev_server_name_input.update(cx, |input, cx| {
508 input.set_disabled(*creating || dev_server.is_some(), cx);
509 });
510
511 v_flex()
512 .id("scroll-container")
513 .h_full()
514 .overflow_y_scroll()
515 .track_scroll(&self.scroll_handle)
516 .px_1()
517 .pt_0p5()
518 .gap_px()
519 .child(
520 ModalHeader::new("remote-projects")
521 .show_back_button(true)
522 .child(Headline::new("New dev server").size(HeadlineSize::Small)),
523 )
524 .child(
525 ModalContent::new().child(
526 v_flex()
527 .w_full()
528 .child(
529 h_flex()
530 .pb_2()
531 .items_end()
532 .w_full()
533 .px_2()
534 .border_b_1()
535 .border_color(cx.theme().colors().border)
536 .child(
537 div()
538 .pl_2()
539 .max_w(rems(16.))
540 .child(self.dev_server_name_input.clone()),
541 )
542 .child(
543 div()
544 .pl_1()
545 .pb(px(3.))
546 .when(!*creating && dev_server.is_none(), |div| {
547 div.child(Button::new("create-dev-server", "Create").on_click(
548 cx.listener(move |this, _, cx| {
549 this.create_dev_server(cx);
550 }),
551 ))
552 })
553 .when(*creating && dev_server.is_none(), |div| {
554 div.child(
555 Button::new("create-dev-server", "Creating...")
556 .disabled(true),
557 )
558 }),
559 )
560 )
561 .when(dev_server.is_none(), |div| {
562 div.px_2().child(Label::new("Once you have created a dev server, you will be given a command to run on the server to register it.").color(Color::Muted))
563 })
564 .when_some(dev_server.clone(), |div, dev_server| {
565 let status = self
566 .dev_server_store
567 .read(cx)
568 .dev_server_status(DevServerId(dev_server.dev_server_id));
569
570 let instructions = SharedString::from(format!(
571 "zed --dev-server-token {}",
572 dev_server.access_token
573 ));
574 div.child(
575 v_flex()
576 .pl_2()
577 .pt_2()
578 .gap_2()
579 .child(
580 h_flex().justify_between().w_full()
581 .child(Label::new(format!(
582 "Please log into `{}` and run:",
583 dev_server.name
584 )))
585 .child(
586 Button::new("copy-access-token", "Copy Instructions")
587 .icon(Some(IconName::Copy))
588 .icon_size(IconSize::Small)
589 .on_click({
590 let instructions = instructions.clone();
591 cx.listener(move |_, _, cx| {
592 cx.write_to_clipboard(ClipboardItem::new(
593 instructions.to_string(),
594 ))
595 })})
596 )
597 )
598 .child(
599 v_flex()
600 .w_full()
601 .bg(cx.theme().colors().title_bar_background) // todo: this should be distinct
602 .border()
603 .border_color(cx.theme().colors().border_variant)
604 .rounded_md()
605 .my_1()
606 .py_0p5()
607 .px_3()
608 .font_family(ThemeSettings::get_global(cx).buffer_font.family.clone())
609 .child(Label::new(instructions))
610 )
611 .when(status == DevServerStatus::Offline, |this| {
612 this.child(
613
614 h_flex()
615 .gap_2()
616 .child(
617 Icon::new(IconName::ArrowCircle)
618 .size(IconSize::Medium)
619 .with_animation(
620 "arrow-circle",
621 Animation::new(Duration::from_secs(2)).repeat(),
622 |icon, delta| {
623 icon.transform(Transformation::rotate(percentage(delta)))
624 },
625 ),
626 )
627 .child(
628 Label::new("Waiting for connection…"),
629 )
630 )
631 })
632 .when(status == DevServerStatus::Online, |this| {
633 this.child(Label::new("🎊 Connection established!"))
634 .child(
635 h_flex().justify_end().child(
636 Button::new("done", "Done").on_click(cx.listener(
637 |_, _, cx| {
638 cx.dispatch_action(menu::Cancel.boxed_clone())
639 },
640 ))
641 ),
642 )
643 }),
644 )
645 }),
646 )
647 )
648 }
649
650 fn render_default(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
651 let dev_servers = self.dev_server_store.read(cx).dev_servers();
652
653 let Mode::Default(create_dev_server_project) = &self.mode else {
654 unreachable!()
655 };
656 let create_dev_server_project = create_dev_server_project.clone();
657
658 v_flex()
659 .id("scroll-container")
660 .h_full()
661 .overflow_y_scroll()
662 .track_scroll(&self.scroll_handle)
663 .px_1()
664 .pt_0p5()
665 .gap_px()
666 .child(
667 ModalHeader::new("remote-projects")
668 .show_dismiss_button(true)
669 .child(Headline::new("Remote Projects").size(HeadlineSize::Small)),
670 )
671 .child(
672 ModalContent::new().child(
673 List::new()
674 .empty_message("No dev servers registered.")
675 .header(Some(
676 ListHeader::new("Dev Servers").end_slot(
677 Button::new("register-dev-server-button", "New Server")
678 .icon(IconName::Plus)
679 .icon_position(IconPosition::Start)
680 .tooltip(|cx| Tooltip::text("Register a new dev server", cx))
681 .on_click(cx.listener(|this, _, cx| {
682 this.mode = Mode::CreateDevServer(Default::default());
683
684 this.dev_server_name_input.update(cx, |input, cx| {
685 input.editor().update(cx, |editor, cx| {
686 editor.set_text("", cx);
687 });
688 input.focus_handle(cx).focus(cx)
689 });
690
691 cx.notify();
692 })),
693 ),
694 ))
695 .children(dev_servers.iter().map(|dev_server| {
696 self.render_dev_server(
697 dev_server,
698 create_dev_server_project.clone(),
699 cx,
700 )
701 .into_any_element()
702 })),
703 ),
704 )
705 }
706
707 // fn render_create_dev_server_project(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
708 // let Mode::CreateDevServerProject(CreateDevServerProject {
709 // dev_server_id,
710 // creating,
711 // dev_server_project,
712 // }) = &self.mode
713 // else {
714 // unreachable!()
715 // };
716
717 // let dev_server = self
718 // .dev_server_store
719 // .read(cx)
720 // .dev_server(*dev_server_id)
721 // .cloned();
722
723 // let (dev_server_name, dev_server_status) = dev_server
724 // .map(|server| (server.name, server.status))
725 // .unwrap_or((SharedString::from(""), DevServerStatus::Offline));
726
727 // v_flex()
728 // .px_1()
729 // .pt_0p5()
730 // .gap_px()
731 // .child(
732 // v_flex().py_0p5().px_1().child(
733 // h_flex()
734 // .px_1()
735 // .py_0p5()
736 // .child(
737 // IconButton::new("back", IconName::ArrowLeft)
738 // .style(ButtonStyle::Transparent)
739 // .on_click(cx.listener(|_, _: &gpui::ClickEvent, cx| {
740 // cx.dispatch_action(menu::Cancel.boxed_clone())
741 // })),
742 // )
743 // .child(Headline::new("Add remote project").size(HeadlineSize::Small)),
744 // ),
745 // )
746 // .child(
747 // h_flex()
748 // .ml_5()
749 // .gap_2()
750 // .child(
751 // div()
752 // .id(("status", dev_server_id.0))
753 // .relative()
754 // .child(Icon::new(IconName::Server))
755 // .child(div().absolute().bottom_0().left(rems_from_px(12.0)).child(
756 // Indicator::dot().color(match dev_server_status {
757 // DevServerStatus::Online => Color::Created,
758 // DevServerStatus::Offline => Color::Hidden,
759 // }),
760 // ))
761 // .tooltip(move |cx| {
762 // Tooltip::text(
763 // match dev_server_status {
764 // DevServerStatus::Online => "Online",
765 // DevServerStatus::Offline => "Offline",
766 // },
767 // cx,
768 // )
769 // }),
770 // )
771 // .child(dev_server_name.clone()),
772 // )
773 // .child(
774 // h_flex()
775 // .ml_5()
776 // .gap_2()
777 // .child(self.project_path_input.clone())
778 // .when(!*creating && dev_server_project.is_none(), |div| {
779 // div.child(Button::new("create-remote-server", "Create").on_click({
780 // let dev_server_id = *dev_server_id;
781 // cx.listener(move |this, _, cx| {
782 // this.create_dev_server_project(dev_server_id, cx)
783 // })
784 // }))
785 // })
786 // .when(*creating, |div| {
787 // div.child(Button::new("create-dev-server", "Creating...").disabled(true))
788 // }),
789 // )
790 // .when_some(dev_server_project.clone(), |div, dev_server_project| {
791 // let status = self
792 // .dev_server_store
793 // .read(cx)
794 // .dev_server_project(DevServerProjectId(dev_server_project.id))
795 // .map(|project| {
796 // if project.project_id.is_some() {
797 // DevServerStatus::Online
798 // } else {
799 // DevServerStatus::Offline
800 // }
801 // })
802 // .unwrap_or(DevServerStatus::Offline);
803 // div.child(
804 // v_flex()
805 // .ml_5()
806 // .ml_8()
807 // .gap_2()
808 // .when(status == DevServerStatus::Offline, |this| {
809 // this.child(Label::new("Waiting for project..."))
810 // })
811 // .when(status == DevServerStatus::Online, |this| {
812 // this.child(Label::new("Project online! 🎊")).child(
813 // Button::new("done", "Done").on_click(cx.listener(|_, _, cx| {
814 // cx.dispatch_action(menu::Cancel.boxed_clone())
815 // })),
816 // )
817 // }),
818 // )
819 // })
820 // }
821}
822impl ModalView for DevServerProjects {}
823
824impl FocusableView for DevServerProjects {
825 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
826 self.focus_handle.clone()
827 }
828}
829
830impl EventEmitter<DismissEvent> for DevServerProjects {}
831
832impl Render for DevServerProjects {
833 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
834 div()
835 .track_focus(&self.focus_handle)
836 .elevation_3(cx)
837 .key_context("DevServerModal")
838 .on_action(cx.listener(Self::cancel))
839 .on_action(cx.listener(Self::confirm))
840 .on_mouse_down_out(cx.listener(|this, _, cx| {
841 if matches!(this.mode, Mode::Default(None)) {
842 cx.emit(DismissEvent)
843 }
844 }))
845 .pb_4()
846 .w(rems(34.))
847 .min_h(rems(20.))
848 .max_h(rems(40.))
849 .child(match &self.mode {
850 Mode::Default(_) => self.render_default(cx).into_any_element(),
851 Mode::CreateDevServer(_) => self.render_create_dev_server(cx).into_any_element(),
852 })
853 }
854}