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