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, cx| match &result {
196 Ok(dev_server) => {
197 this.focus_handle.focus(cx);
198 this.mode = Mode::CreateDevServer(CreateDevServer {
199 creating: false,
200 dev_server: Some(dev_server.clone()),
201 });
202 }
203 Err(_) => {
204 this.mode = Mode::CreateDevServer(Default::default());
205 }
206 })
207 .log_err();
208 result
209 })
210 .detach_and_prompt_err("Failed to create server", cx, |_, _| None);
211
212 self.mode = Mode::CreateDevServer(CreateDevServer {
213 creating: true,
214 dev_server: None,
215 });
216 cx.notify()
217 }
218
219 fn delete_dev_server(&mut self, id: DevServerId, cx: &mut ViewContext<Self>) {
220 let answer = cx.prompt(
221 gpui::PromptLevel::Destructive,
222 "Are you sure?",
223 Some("This will delete the dev server and all of its remote projects."),
224 &["Delete", "Cancel"],
225 );
226
227 cx.spawn(|this, mut cx| async move {
228 let answer = answer.await?;
229
230 if answer != 0 {
231 return Ok(());
232 }
233
234 let project_ids: Vec<DevServerProjectId> = this.update(&mut cx, |this, cx| {
235 this.dev_server_store.update(cx, |store, _| {
236 store
237 .projects_for_server(id)
238 .into_iter()
239 .map(|project| project.id)
240 .collect()
241 })
242 })?;
243
244 this.update(&mut cx, |this, cx| {
245 this.dev_server_store
246 .update(cx, |store, cx| store.delete_dev_server(id, cx))
247 })?
248 .await?;
249
250 for id in project_ids {
251 WORKSPACE_DB
252 .delete_workspace_by_dev_server_project_id(id)
253 .await
254 .log_err();
255 }
256 Ok(())
257 })
258 .detach_and_prompt_err("Failed to delete dev server", cx, |_, _| None);
259 }
260
261 fn delete_dev_server_project(
262 &mut self,
263 id: DevServerProjectId,
264 path: &str,
265 cx: &mut ViewContext<Self>,
266 ) {
267 let answer = cx.prompt(
268 gpui::PromptLevel::Destructive,
269 format!("Delete \"{}\"?", path).as_str(),
270 Some("This will delete the remote project. You can always re-add it later."),
271 &["Delete", "Cancel"],
272 );
273
274 cx.spawn(|this, mut cx| async move {
275 let answer = answer.await?;
276
277 if answer != 0 {
278 return Ok(());
279 }
280
281 this.update(&mut cx, |this, cx| {
282 this.dev_server_store
283 .update(cx, |store, cx| store.delete_dev_server_project(id, cx))
284 })?
285 .await?;
286
287 WORKSPACE_DB
288 .delete_workspace_by_dev_server_project_id(id)
289 .await
290 .log_err();
291
292 Ok(())
293 })
294 .detach_and_prompt_err("Failed to delete dev server project", cx, |_, _| None);
295 }
296
297 fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
298 match &self.mode {
299 Mode::Default(None) => {}
300 Mode::Default(Some(create_project)) => {
301 self.create_dev_server_project(create_project.dev_server_id, cx);
302 }
303 Mode::CreateDevServer(state) => {
304 if !state.creating && state.dev_server.is_none() {
305 self.create_dev_server(cx);
306 }
307 }
308 }
309 }
310
311 fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
312 match self.mode {
313 Mode::Default(None) => cx.emit(DismissEvent),
314 _ => {
315 self.mode = Mode::Default(None);
316 self.focus_handle(cx).focus(cx);
317 cx.notify();
318 }
319 }
320 }
321
322 fn render_dev_server(
323 &mut self,
324 dev_server: &DevServer,
325 mut create_project: Option<CreateDevServerProject>,
326 cx: &mut ViewContext<Self>,
327 ) -> impl IntoElement {
328 let dev_server_id = dev_server.id;
329 let status = dev_server.status;
330 if create_project
331 .as_ref()
332 .is_some_and(|cp| cp.dev_server_id != dev_server.id)
333 {
334 create_project = None;
335 }
336
337 v_flex()
338 .w_full()
339 .child(
340 h_flex()
341 .group("dev-server")
342 .justify_between()
343 .child(
344 h_flex()
345 .gap_2()
346 .child(
347 div()
348 .id(("status", dev_server.id.0))
349 .relative()
350 .child(Icon::new(IconName::Server).size(IconSize::Small))
351 .child(
352 div().absolute().bottom_0().left(rems_from_px(8.0)).child(
353 Indicator::dot().color(match status {
354 DevServerStatus::Online => Color::Created,
355 DevServerStatus::Offline => Color::Hidden,
356 }),
357 ),
358 )
359 .tooltip(move |cx| {
360 Tooltip::text(
361 match status {
362 DevServerStatus::Online => "Online",
363 DevServerStatus::Offline => "Offline",
364 },
365 cx,
366 )
367 }),
368 )
369 .child(dev_server.name.clone())
370 .child(
371 h_flex()
372 .visible_on_hover("dev-server")
373 .gap_1()
374 .child(
375 IconButton::new("edit-dev-server", IconName::Pencil)
376 .disabled(true) //TODO implement this on the collab side
377 .tooltip(|cx| {
378 Tooltip::text("Coming Soon - Edit dev server", cx)
379 }),
380 )
381 .child({
382 let dev_server_id = dev_server.id;
383 IconButton::new("remove-dev-server", IconName::Trash)
384 .on_click(cx.listener(move |this, _, cx| {
385 this.delete_dev_server(dev_server_id, cx)
386 }))
387 .tooltip(|cx| Tooltip::text("Remove dev server", cx))
388 }),
389 ),
390 )
391 .child(
392 h_flex().gap_1().child(
393 IconButton::new(
394 ("add-remote-project", dev_server_id.0),
395 IconName::Plus,
396 )
397 .tooltip(|cx| Tooltip::text("Add a remote project", cx))
398 .on_click(cx.listener(
399 move |this, _, cx| {
400 if let Mode::Default(project) = &mut this.mode {
401 *project = Some(CreateDevServerProject {
402 dev_server_id,
403 creating: false,
404 });
405 }
406 this.project_path_input.read(cx).focus_handle(cx).focus(cx);
407 cx.notify();
408 },
409 )),
410 ),
411 ),
412 )
413 .child(
414 v_flex()
415 .w_full()
416 .bg(cx.theme().colors().title_bar_background) // todo: this should be distinct
417 .border()
418 .border_color(cx.theme().colors().border_variant)
419 .rounded_md()
420 .my_1()
421 .py_0p5()
422 .px_3()
423 .child(
424 List::new()
425 .empty_message("No projects.")
426 .children(
427 self.dev_server_store
428 .read(cx)
429 .projects_for_server(dev_server.id)
430 .iter()
431 .map(|p| self.render_dev_server_project(p, cx)),
432 )
433 .when_some(create_project, |el, create_project| {
434 el.child(self.render_create_new_project(&create_project, cx))
435 }),
436 ),
437 )
438 }
439
440 fn render_create_new_project(
441 &mut self,
442 create_project: &CreateDevServerProject,
443 _: &mut ViewContext<Self>,
444 ) -> impl IntoElement {
445 ListItem::new("create-remote-project")
446 .start_slot(Icon::new(IconName::FileTree).color(Color::Muted))
447 .child(self.project_path_input.clone())
448 .child(
449 div()
450 .w(IconSize::Medium.rems())
451 .when(create_project.creating, |el| {
452 el.child(
453 Icon::new(IconName::ArrowCircle)
454 .size(IconSize::Medium)
455 .with_animation(
456 "arrow-circle",
457 Animation::new(Duration::from_secs(2)).repeat(),
458 |icon, delta| {
459 icon.transform(Transformation::rotate(percentage(delta)))
460 },
461 ),
462 )
463 }),
464 )
465 }
466
467 fn render_dev_server_project(
468 &mut self,
469 project: &DevServerProject,
470 cx: &mut ViewContext<Self>,
471 ) -> impl IntoElement {
472 let dev_server_project_id = project.id;
473 let project_id = project.project_id;
474 let is_online = project_id.is_some();
475 let project_path = project.path.clone();
476
477 ListItem::new(("remote-project", dev_server_project_id.0))
478 .start_slot(Icon::new(IconName::FileTree).when(!is_online, |icon| icon.color(Color::Muted)))
479 .child(
480 Label::new(project.path.clone())
481 )
482 .on_click(cx.listener(move |_, _, cx| {
483 if let Some(project_id) = project_id {
484 if let Some(app_state) = AppState::global(cx).upgrade() {
485 workspace::join_dev_server_project(project_id, app_state, None, cx)
486 .detach_and_prompt_err("Could not join project", cx, |_, _| None)
487 }
488 } else {
489 cx.spawn(|_, mut cx| async move {
490 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();
491 }).detach();
492 }
493 }))
494 .end_hover_slot::<AnyElement>(Some(IconButton::new("remove-remote-project", IconName::Trash)
495 .on_click(cx.listener(move |this, _, cx| {
496 this.delete_dev_server_project(dev_server_project_id, &project_path, cx)
497 }))
498 .tooltip(|cx| Tooltip::text("Delete remote project", cx)).into_any_element()))
499 }
500
501 fn render_create_dev_server(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
502 let Mode::CreateDevServer(CreateDevServer {
503 creating,
504 dev_server,
505 }) = &self.mode
506 else {
507 unreachable!()
508 };
509
510 self.dev_server_name_input.update(cx, |input, cx| {
511 input.set_disabled(*creating || dev_server.is_some(), cx);
512 });
513
514 v_flex()
515 .id("scroll-container")
516 .h_full()
517 .overflow_y_scroll()
518 .track_scroll(&self.scroll_handle)
519 .px_1()
520 .pt_0p5()
521 .gap_px()
522 .child(
523 ModalHeader::new("remote-projects")
524 .show_back_button(true)
525 .child(Headline::new("New dev server").size(HeadlineSize::Small)),
526 )
527 .child(
528 ModalContent::new().child(
529 v_flex()
530 .w_full()
531 .child(
532 h_flex()
533 .pb_2()
534 .items_end()
535 .w_full()
536 .px_2()
537 .border_b_1()
538 .border_color(cx.theme().colors().border)
539 .child(
540 div()
541 .pl_2()
542 .max_w(rems(16.))
543 .child(self.dev_server_name_input.clone()),
544 )
545 .child(
546 div()
547 .pl_1()
548 .pb(px(3.))
549 .when(!*creating && dev_server.is_none(), |div| {
550 div.child(Button::new("create-dev-server", "Create").on_click(
551 cx.listener(move |this, _, cx| {
552 this.create_dev_server(cx);
553 }),
554 ))
555 })
556 .when(*creating && dev_server.is_none(), |div| {
557 div.child(
558 Button::new("create-dev-server", "Creating...")
559 .disabled(true),
560 )
561 }),
562 )
563 )
564 .when(dev_server.is_none(), |div| {
565 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))
566 })
567 .when_some(dev_server.clone(), |div, dev_server| {
568 let status = self
569 .dev_server_store
570 .read(cx)
571 .dev_server_status(DevServerId(dev_server.dev_server_id));
572
573 let instructions = SharedString::from(format!(
574 "zed --dev-server-token {}",
575 dev_server.access_token
576 ));
577 div.child(
578 v_flex()
579 .pl_2()
580 .pt_2()
581 .gap_2()
582 .child(
583 h_flex().justify_between().w_full()
584 .child(Label::new(format!(
585 "Please log into `{}` and run:",
586 dev_server.name
587 )))
588 .child(
589 Button::new("copy-access-token", "Copy Instructions")
590 .icon(Some(IconName::Copy))
591 .icon_size(IconSize::Small)
592 .on_click({
593 let instructions = instructions.clone();
594 cx.listener(move |_, _, cx| {
595 cx.write_to_clipboard(ClipboardItem::new(
596 instructions.to_string(),
597 ))
598 })})
599 )
600 )
601 .child(
602 v_flex()
603 .w_full()
604 .bg(cx.theme().colors().title_bar_background) // todo: this should be distinct
605 .border()
606 .border_color(cx.theme().colors().border_variant)
607 .rounded_md()
608 .my_1()
609 .py_0p5()
610 .px_3()
611 .font_family(ThemeSettings::get_global(cx).buffer_font.family.clone())
612 .child(Label::new(instructions))
613 )
614 .when(status == DevServerStatus::Offline, |this| {
615 this.child(
616
617 h_flex()
618 .gap_2()
619 .child(
620 Icon::new(IconName::ArrowCircle)
621 .size(IconSize::Medium)
622 .with_animation(
623 "arrow-circle",
624 Animation::new(Duration::from_secs(2)).repeat(),
625 |icon, delta| {
626 icon.transform(Transformation::rotate(percentage(delta)))
627 },
628 ),
629 )
630 .child(
631 Label::new("Waiting for connection…"),
632 )
633 )
634 })
635 .when(status == DevServerStatus::Online, |this| {
636 this.child(Label::new("🎊 Connection established!"))
637 .child(
638 h_flex().justify_end().child(
639 Button::new("done", "Done").on_click(cx.listener(
640 |_, _, cx| {
641 cx.dispatch_action(menu::Cancel.boxed_clone())
642 },
643 ))
644 ),
645 )
646 }),
647 )
648 }),
649 )
650 )
651 }
652
653 fn render_default(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
654 let dev_servers = self.dev_server_store.read(cx).dev_servers();
655
656 let Mode::Default(create_dev_server_project) = &self.mode else {
657 unreachable!()
658 };
659 let create_dev_server_project = create_dev_server_project.clone();
660
661 v_flex()
662 .id("scroll-container")
663 .h_full()
664 .overflow_y_scroll()
665 .track_scroll(&self.scroll_handle)
666 .px_1()
667 .pt_0p5()
668 .gap_px()
669 .child(
670 ModalHeader::new("remote-projects")
671 .show_dismiss_button(true)
672 .child(Headline::new("Remote Projects").size(HeadlineSize::Small)),
673 )
674 .child(
675 ModalContent::new().child(
676 List::new()
677 .empty_message("No dev servers registered.")
678 .header(Some(
679 ListHeader::new("Dev Servers").end_slot(
680 Button::new("register-dev-server-button", "New Server")
681 .icon(IconName::Plus)
682 .icon_position(IconPosition::Start)
683 .tooltip(|cx| Tooltip::text("Register a new dev server", cx))
684 .on_click(cx.listener(|this, _, cx| {
685 this.mode = Mode::CreateDevServer(Default::default());
686
687 this.dev_server_name_input.update(cx, |input, cx| {
688 input.editor().update(cx, |editor, cx| {
689 editor.set_text("", cx);
690 });
691 input.focus_handle(cx).focus(cx)
692 });
693
694 cx.notify();
695 })),
696 ),
697 ))
698 .children(dev_servers.iter().map(|dev_server| {
699 self.render_dev_server(
700 dev_server,
701 create_dev_server_project.clone(),
702 cx,
703 )
704 .into_any_element()
705 })),
706 ),
707 )
708 }
709
710 // fn render_create_dev_server_project(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
711 // let Mode::CreateDevServerProject(CreateDevServerProject {
712 // dev_server_id,
713 // creating,
714 // dev_server_project,
715 // }) = &self.mode
716 // else {
717 // unreachable!()
718 // };
719
720 // let dev_server = self
721 // .dev_server_store
722 // .read(cx)
723 // .dev_server(*dev_server_id)
724 // .cloned();
725
726 // let (dev_server_name, dev_server_status) = dev_server
727 // .map(|server| (server.name, server.status))
728 // .unwrap_or((SharedString::from(""), DevServerStatus::Offline));
729
730 // v_flex()
731 // .px_1()
732 // .pt_0p5()
733 // .gap_px()
734 // .child(
735 // v_flex().py_0p5().px_1().child(
736 // h_flex()
737 // .px_1()
738 // .py_0p5()
739 // .child(
740 // IconButton::new("back", IconName::ArrowLeft)
741 // .style(ButtonStyle::Transparent)
742 // .on_click(cx.listener(|_, _: &gpui::ClickEvent, cx| {
743 // cx.dispatch_action(menu::Cancel.boxed_clone())
744 // })),
745 // )
746 // .child(Headline::new("Add remote project").size(HeadlineSize::Small)),
747 // ),
748 // )
749 // .child(
750 // h_flex()
751 // .ml_5()
752 // .gap_2()
753 // .child(
754 // div()
755 // .id(("status", dev_server_id.0))
756 // .relative()
757 // .child(Icon::new(IconName::Server))
758 // .child(div().absolute().bottom_0().left(rems_from_px(12.0)).child(
759 // Indicator::dot().color(match dev_server_status {
760 // DevServerStatus::Online => Color::Created,
761 // DevServerStatus::Offline => Color::Hidden,
762 // }),
763 // ))
764 // .tooltip(move |cx| {
765 // Tooltip::text(
766 // match dev_server_status {
767 // DevServerStatus::Online => "Online",
768 // DevServerStatus::Offline => "Offline",
769 // },
770 // cx,
771 // )
772 // }),
773 // )
774 // .child(dev_server_name.clone()),
775 // )
776 // .child(
777 // h_flex()
778 // .ml_5()
779 // .gap_2()
780 // .child(self.project_path_input.clone())
781 // .when(!*creating && dev_server_project.is_none(), |div| {
782 // div.child(Button::new("create-remote-server", "Create").on_click({
783 // let dev_server_id = *dev_server_id;
784 // cx.listener(move |this, _, cx| {
785 // this.create_dev_server_project(dev_server_id, cx)
786 // })
787 // }))
788 // })
789 // .when(*creating, |div| {
790 // div.child(Button::new("create-dev-server", "Creating...").disabled(true))
791 // }),
792 // )
793 // .when_some(dev_server_project.clone(), |div, dev_server_project| {
794 // let status = self
795 // .dev_server_store
796 // .read(cx)
797 // .dev_server_project(DevServerProjectId(dev_server_project.id))
798 // .map(|project| {
799 // if project.project_id.is_some() {
800 // DevServerStatus::Online
801 // } else {
802 // DevServerStatus::Offline
803 // }
804 // })
805 // .unwrap_or(DevServerStatus::Offline);
806 // div.child(
807 // v_flex()
808 // .ml_5()
809 // .ml_8()
810 // .gap_2()
811 // .when(status == DevServerStatus::Offline, |this| {
812 // this.child(Label::new("Waiting for project..."))
813 // })
814 // .when(status == DevServerStatus::Online, |this| {
815 // this.child(Label::new("Project online! 🎊")).child(
816 // Button::new("done", "Done").on_click(cx.listener(|_, _, cx| {
817 // cx.dispatch_action(menu::Cancel.boxed_clone())
818 // })),
819 // )
820 // }),
821 // )
822 // })
823 // }
824}
825impl ModalView for DevServerProjects {}
826
827impl FocusableView for DevServerProjects {
828 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
829 self.focus_handle.clone()
830 }
831}
832
833impl EventEmitter<DismissEvent> for DevServerProjects {}
834
835impl Render for DevServerProjects {
836 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
837 div()
838 .track_focus(&self.focus_handle)
839 .elevation_3(cx)
840 .key_context("DevServerModal")
841 .on_action(cx.listener(Self::cancel))
842 .on_action(cx.listener(Self::confirm))
843 .on_mouse_down_out(cx.listener(|this, _, cx| {
844 if matches!(this.mode, Mode::Default(None)) {
845 cx.emit(DismissEvent)
846 }
847 }))
848 .pb_4()
849 .w(rems(34.))
850 .min_h(rems(20.))
851 .max_h(rems(40.))
852 .child(match &self.mode {
853 Mode::Default(_) => self.render_default(cx).into_any_element(),
854 Mode::CreateDevServer(_) => self.render_create_dev_server(cx).into_any_element(),
855 })
856 }
857}