Make modal behavior stateless

Mikayla Maki and antonio created

Co-authored-by: antonio <antonio@zed.dev>

Change summary

crates/copilot/src/copilot.rs |   1 
crates/copilot/src/sign_in.rs | 520 +++++++++++++++++++++---------------
2 files changed, 303 insertions(+), 218 deletions(-)

Detailed changes

crates/copilot/src/copilot.rs 🔗

@@ -226,6 +226,7 @@ impl Copilot {
         if let CopilotServer::Started { server, status } = &mut self.server {
             let task = match status {
                 SignInStatus::Authorized { .. } | SignInStatus::Unauthorized { .. } => {
+                    cx.notify();
                     Task::ready(Ok(())).shared()
                 }
                 SignInStatus::SigningIn { task, .. } => {

crates/copilot/src/sign_in.rs 🔗

@@ -1,7 +1,9 @@
-use crate::{request::PromptUserDeviceFlow, Copilot};
+use crate::{request::PromptUserDeviceFlow, Copilot, Status};
 use gpui::{
-    elements::*, geometry::rect::RectF, ClipboardItem, Element, Entity, MutableAppContext, View,
-    WindowKind, WindowOptions,
+    elements::*,
+    geometry::{rect::RectF, vector::vec2f},
+    ClipboardItem, Element, Entity, MutableAppContext, View, ViewContext, ViewHandle, WindowKind,
+    WindowOptions,
 };
 use settings::Settings;
 
@@ -13,158 +15,119 @@ struct OpenGithub;
 
 const _COPILOT_SIGN_UP_URL: &'static str = "https://github.com/features/copilot";
 
-enum SignInContents {
-    PromptingUser(PromptUserDeviceFlow),
-    Unauthorized,
-    Enabled,
-}
-
 pub fn init(cx: &mut MutableAppContext) {
     let copilot = Copilot::global(cx).unwrap();
 
-    let mut code_verification_window_id: Option<(usize, SignInContents)> = None;
+    let mut code_verification: Option<ViewHandle<CopilotCodeVerification>> = None;
     cx.observe(&copilot, move |copilot, cx| {
-        match copilot.read(cx).status() {
-            crate::Status::SigningIn {
-                prompt: Some(prompt),
-            } => {
-                let window_id = match code_verification_window_id.take() {
-                    Some((window_id, SignInContents::PromptingUser(current_prompt)))
-                        if current_prompt == prompt =>
-                    {
-                        if cx.window_ids().find(|item| item == &window_id).is_some() {
-                            window_id
-                        } else {
-                            CopilotCodeVerification::prompting(prompt.clone(), cx)
-                        }
-                    }
-                    Some((window_id, _)) => {
-                        cx.remove_window(window_id);
-                        CopilotCodeVerification::prompting(prompt.clone(), cx)
-                    }
-                    None => CopilotCodeVerification::prompting(prompt.clone(), cx),
-                };
+        let status = copilot.read(cx).status();
 
-                code_verification_window_id =
-                    Some((window_id, SignInContents::PromptingUser(prompt)));
-
-                cx.activate_window(window_id);
-            }
-            crate::Status::Authorized => match code_verification_window_id.take() {
-                Some((window_id, sign_in_contents)) => {
-                    match sign_in_contents {
-                        SignInContents::PromptingUser(_) => cx.remove_window(window_id),
-                        SignInContents::Unauthorized => cx.remove_window(window_id),
-                        SignInContents::Enabled => {
-                            if cx.has_window(window_id) {
-                                code_verification_window_id =
-                                    Some((window_id, SignInContents::Enabled))
-                            }
-                            return;
-                        }
-                    }
-                    let window_id = CopilotCodeVerification::enabled(cx);
-                    code_verification_window_id = Some((window_id, SignInContents::Enabled));
-                    cx.activate_window(window_id);
+        match &status {
+            crate::Status::SigningIn { prompt } => {
+                if let Some(code_verification) = code_verification.as_ref() {
+                    code_verification.update(cx, |code_verification, cx| {
+                        code_verification.set_status(status, cx)
+                    });
+                    cx.activate_window(code_verification.window_id());
+                } else if let Some(_prompt) = prompt {
+                    let window_size = cx.global::<Settings>().theme.copilot.modal.dimensions();
+                    let window_options = WindowOptions {
+                        bounds: gpui::WindowBounds::Fixed(RectF::new(
+                            Default::default(),
+                            window_size,
+                        )),
+                        titlebar: None,
+                        center: true,
+                        focus: true,
+                        kind: WindowKind::Normal,
+                        is_movable: true,
+                        screen: None,
+                    };
+                    let (_, view) =
+                        cx.add_window(window_options, |_cx| CopilotCodeVerification::new(status));
+                    code_verification = Some(view);
                 }
-                None => return,
-            },
-            crate::Status::Unauthorized => match code_verification_window_id.take() {
-                Some((window_id, sign_in_contents)) => {
-                    match sign_in_contents {
-                        SignInContents::PromptingUser(_) => cx.remove_window(window_id), // Show prompt
-                        SignInContents::Unauthorized => {
-                            if cx.has_window(window_id) {
-                                code_verification_window_id =
-                                    Some((window_id, SignInContents::Unauthorized))
-                            }
-                            return;
-                        } //Do nothing
-                        SignInContents::Enabled => cx.remove_window(window_id),          //
-                    }
+            }
+            Status::Authorized | Status::Unauthorized => {
+                if let Some(code_verification) = code_verification.as_ref() {
+                    code_verification.update(cx, |code_verification, cx| {
+                        code_verification.set_status(status, cx)
+                    });
 
-                    let window_id = CopilotCodeVerification::unauthorized(cx);
-                    code_verification_window_id = Some((window_id, SignInContents::Unauthorized));
-                    cx.activate_window(window_id);
+                    cx.platform().activate(true);
+                    cx.activate_window(code_verification.window_id());
                 }
-                None => return,
-            },
+            }
             _ => {
-                if let Some((window_id, _)) = code_verification_window_id.take() {
-                    cx.remove_window(window_id);
+                if let Some(code_verification) = code_verification.take() {
+                    cx.remove_window(code_verification.window_id());
                 }
             }
         }
     })
     .detach();
+
+    // Modal theming test:
+    // let window_size = cx.global::<Settings>().theme.copilot.modal.dimensions();
+    // let window_options = WindowOptions {
+    //     bounds: gpui::WindowBounds::Fixed(RectF::new(Default::default(), window_size)),
+    //     titlebar: None,
+    //     center: false,
+    //     focus: false,
+    //     kind: WindowKind::PopUp,
+    //     is_movable: true,
+    //     screen: None,
+    // };
+    // let (_, _view) = cx.add_window(window_options, |_cx| {
+    //     CopilotCodeVerification::new(Status::SigningIn {
+    //         prompt: Some(PromptUserDeviceFlow {
+    //             user_code: "ABCD-1234".to_string(),
+    //             verification_uri: "https://github.com/login/device".to_string(),
+    //         }),
+    //     })
+    // });
+
+    // let window_size = cx.global::<Settings>().theme.copilot.modal.dimensions();
+    // let window_options = WindowOptions {
+    //     bounds: gpui::WindowBounds::Fixed(RectF::new(vec2f(window_size.x(), 0.), window_size)),
+    //     titlebar: None,
+    //     center: false,
+    //     focus: false,
+    //     kind: WindowKind::PopUp,
+    //     is_movable: true,
+    //     screen: None,
+    // };
+    // let (_, _view) = cx.add_window(window_options, |_cx| {
+    //     CopilotCodeVerification::new(Status::Authorized)
+    // });
+
+    // let window_size = cx.global::<Settings>().theme.copilot.modal.dimensions();
+    // let window_options = WindowOptions {
+    //     bounds: gpui::WindowBounds::Fixed(RectF::new(vec2f(0., window_size.y()), window_size)),
+    //     titlebar: None,
+    //     center: false,
+    //     focus: false,
+    //     kind: WindowKind::PopUp,
+    //     is_movable: true,
+    //     screen: None,
+    // };
+    // let (_, _view) = cx.add_window(window_options, |_cx| {
+    //     CopilotCodeVerification::new(Status::Unauthorized)
+    // });
 }
 
 pub struct CopilotCodeVerification {
-    prompt: SignInContents,
+    status: Status,
 }
 
 impl CopilotCodeVerification {
-    pub fn prompting(prompt: PromptUserDeviceFlow, cx: &mut MutableAppContext) -> usize {
-        let window_size = cx.global::<Settings>().theme.copilot.modal.dimensions();
-
-        let (window_id, _) = cx.add_window(
-            WindowOptions {
-                bounds: gpui::WindowBounds::Fixed(RectF::new(Default::default(), window_size)),
-                titlebar: None,
-                center: true,
-                focus: false,
-                kind: WindowKind::Normal,
-                is_movable: true,
-                screen: None,
-            },
-            |_| CopilotCodeVerification {
-                prompt: SignInContents::PromptingUser(prompt),
-            },
-        );
-
-        window_id
-    }
-
-    pub fn unauthorized(cx: &mut MutableAppContext) -> usize {
-        let window_size = cx.global::<Settings>().theme.copilot.modal.dimensions();
-
-        let (window_id, _) = cx.add_window(
-            WindowOptions {
-                bounds: gpui::WindowBounds::Fixed(RectF::new(Default::default(), window_size)),
-                titlebar: None,
-                center: true,
-                focus: false,
-                kind: WindowKind::Normal,
-                is_movable: true,
-                screen: None,
-            },
-            |_| CopilotCodeVerification {
-                prompt: SignInContents::Unauthorized,
-            },
-        );
-
-        window_id
+    pub fn new(status: Status) -> Self {
+        Self { status }
     }
 
-    pub fn enabled(cx: &mut MutableAppContext) -> usize {
-        let window_size = cx.global::<Settings>().theme.copilot.modal.dimensions();
-
-        let (window_id, _) = cx.add_window(
-            WindowOptions {
-                bounds: gpui::WindowBounds::Fixed(RectF::new(Default::default(), window_size)),
-                titlebar: None,
-                center: true,
-                focus: false,
-                kind: WindowKind::Normal,
-                is_movable: true,
-                screen: None,
-            },
-            |_| CopilotCodeVerification {
-                prompt: SignInContents::Enabled,
-            },
-        );
-
-        window_id
+    pub fn set_status(&mut self, status: Status, cx: &mut ViewContext<Self>) {
+        self.status = status;
+        cx.notify();
     }
 
     fn render_device_code(
@@ -323,28 +286,111 @@ impl CopilotCodeVerification {
             .with_style(style.auth.enabled_hint)
             .boxed()
     }
-}
-
-impl Entity for CopilotCodeVerification {
-    type Event = ();
-}
 
-impl View for CopilotCodeVerification {
-    fn ui_name() -> &'static str {
-        "CopilotCodeVerification"
-    }
+    fn render_prompting_modal(
+        data: &PromptUserDeviceFlow,
+        style: &theme::Copilot,
+        cx: &mut gpui::RenderContext<Self>,
+    ) -> ElementBox {
+        theme::ui::modal("Connect Copilot to Zed", &style.modal, cx, |cx| {
+            Flex::column()
+                .with_children([
+                    Flex::column()
+                        .with_children([
+                            Flex::row()
+                                .with_children([
+                                    theme::ui::svg(&style.auth.copilot_icon).boxed(),
+                                    theme::ui::icon(&style.auth.plus_icon).boxed(),
+                                    theme::ui::svg(&style.auth.zed_icon).boxed(),
+                                ])
+                                .boxed(),
+                            Flex::column()
+                                .with_children([
+                                    Label::new(
+                                        "Enable Copilot by connecting",
+                                        style.auth.enable_text.clone(),
+                                    )
+                                    .boxed(),
+                                    Label::new(
+                                        "your existing license.",
+                                        style.auth.enable_text.clone(),
+                                    )
+                                    .boxed(),
+                                ])
+                                .align_children_center()
+                                .contained()
+                                .with_style(style.auth.enable_group.clone())
+                                .boxed(),
+                        ])
+                        .align_children_center()
+                        .contained()
+                        .with_style(style.auth.header_group)
+                        .aligned()
+                        .boxed(),
+                    Self::render_device_code(data, &style, cx),
+                    // match &self.prompt {
+                    //     SignInContents::PromptingUser(data) => {
 
-    fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut gpui::ViewContext<Self>) {
-        cx.notify()
-    }
+                    //     }
+                    //     SignInContents::Unauthorized => Self::render_not_authorized_warning(&style),
+                    //     SignInContents::Enabled => Self::render_copilot_enabled(&style),
+                    // },
+                    Flex::column()
+                        .with_child(
+                            theme::ui::cta_button_with_click(
+                                "Connect to GitHub",
+                                style.auth.content_width,
+                                &style.auth.cta_button,
+                                cx,
+                                {
+                                    let verification_uri = data.verification_uri.clone();
+                                    move |_, cx| cx.platform().open_url(&verification_uri)
+                                },
+                            ),
+                            // {
+                            // match &self.prompt {
+                            //     SignInContents::PromptingUser(data) => {
 
-    fn focus_out(&mut self, _: gpui::AnyViewHandle, cx: &mut gpui::ViewContext<Self>) {
-        cx.notify()
+                            //     }
+                            //     // SignInContents::Unauthorized => theme::ui::cta_button_with_click(
+                            //     //     "Close",
+                            //     //     style.auth.content_width,
+                            //     //     &style.auth.cta_button,
+                            //     //     cx,
+                            //     //     |_, cx| {
+                            //     //         let window_id = cx.window_id();
+                            //     //         cx.remove_window(window_id)
+                            //     //     },
+                            //     // ),
+                            //     // SignInContents::Enabled => theme::ui::cta_button_with_click(
+                            //     //     "Done",
+                            //     //     style.auth.content_width,
+                            //     //     &style.auth.cta_button,
+                            //     //     cx,
+                            //     //     |_, cx| {
+                            //     //         let window_id = cx.window_id();
+                            //     //         cx.remove_window(window_id)
+                            //     //     },
+                            //     // ),
+                            // }
+                        )
+                        .align_children_center()
+                        .contained()
+                        .with_style(style.auth.github_group)
+                        .aligned()
+                        .boxed(),
+                ])
+                .align_children_center()
+                .constrained()
+                .with_width(style.auth.content_width)
+                .aligned()
+                .boxed()
+        })
     }
-
-    fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox {
-        let style = cx.global::<Settings>().theme.copilot.clone();
-
+    fn render_enabled_modal(
+        style: &theme::Copilot,
+        cx: &mut gpui::RenderContext<Self>,
+    ) -> ElementBox {
         theme::ui::modal("Connect Copilot to Zed", &style.modal, cx, |cx| {
             Flex::column()
                 .with_children([
@@ -357,81 +403,89 @@ impl View for CopilotCodeVerification {
                                     theme::ui::svg(&style.auth.zed_icon).boxed(),
                                 ])
                                 .boxed(),
-                            match self.prompt {
-                                SignInContents::PromptingUser(_) | SignInContents::Unauthorized => {
-                                    Flex::column()
-                                        .with_children([
-                                            Label::new(
-                                                "Enable Copilot by connecting",
-                                                style.auth.enable_text.clone(),
-                                            )
-                                            .boxed(),
-                                            Label::new(
-                                                "your existing license.",
-                                                style.auth.enable_text.clone(),
-                                            )
-                                            .boxed(),
-                                        ])
-                                        .align_children_center()
-                                        .contained()
-                                        .with_style(style.auth.enable_group.clone())
-                                        .boxed()
-                                }
-                                SignInContents::Enabled => {
-                                    Label::new("Copilot Enabled!", style.auth.enable_text.clone())
-                                        .boxed()
-                                }
-                            },
+                            Label::new("Copilot Enabled!", style.auth.enable_text.clone()).boxed(),
                         ])
                         .align_children_center()
                         .contained()
                         .with_style(style.auth.header_group)
                         .aligned()
                         .boxed(),
-                    match &self.prompt {
-                        SignInContents::PromptingUser(data) => {
-                            Self::render_device_code(data, &style, cx)
-                        }
-                        SignInContents::Unauthorized => Self::render_not_authorized_warning(&style),
-                        SignInContents::Enabled => Self::render_copilot_enabled(&style),
-                    },
+                    Self::render_copilot_enabled(&style),
+                    Flex::column()
+                        .with_child(theme::ui::cta_button_with_click(
+                            "Close",
+                            style.auth.content_width,
+                            &style.auth.cta_button,
+                            cx,
+                            |_, cx| {
+                                let window_id = cx.window_id();
+                                cx.remove_window(window_id)
+                            },
+                        ))
+                        .align_children_center()
+                        .contained()
+                        .with_style(style.auth.github_group)
+                        .aligned()
+                        .boxed(),
+                ])
+                .align_children_center()
+                .constrained()
+                .with_width(style.auth.content_width)
+                .aligned()
+                .boxed()
+        })
+    }
+    fn render_unauthorized_modal(
+        style: &theme::Copilot,
+        cx: &mut gpui::RenderContext<Self>,
+    ) -> ElementBox {
+        theme::ui::modal("Connect Copilot to Zed", &style.modal, cx, |cx| {
+            Flex::column()
+                .with_children([
                     Flex::column()
-                        .with_child({
-                            match &self.prompt {
-                                SignInContents::PromptingUser(data) => {
-                                    theme::ui::cta_button_with_click(
-                                        "Connect to GitHub",
-                                        style.auth.content_width,
-                                        &style.auth.cta_button,
-                                        cx,
-                                        {
-                                            let verification_uri = data.verification_uri.clone();
-                                            move |_, cx| cx.platform().open_url(&verification_uri)
-                                        },
+                        .with_children([
+                            Flex::row()
+                                .with_children([
+                                    theme::ui::svg(&style.auth.copilot_icon).boxed(),
+                                    theme::ui::icon(&style.auth.plus_icon).boxed(),
+                                    theme::ui::svg(&style.auth.zed_icon).boxed(),
+                                ])
+                                .boxed(),
+                            Flex::column()
+                                .with_children([
+                                    Label::new(
+                                        "Enable Copilot by connecting",
+                                        style.auth.enable_text.clone(),
+                                    )
+                                    .boxed(),
+                                    Label::new(
+                                        "your existing license.",
+                                        style.auth.enable_text.clone(),
                                     )
-                                }
-                                SignInContents::Unauthorized => theme::ui::cta_button_with_click(
-                                    "Close",
-                                    style.auth.content_width,
-                                    &style.auth.cta_button,
-                                    cx,
-                                    |_, cx| {
-                                        let window_id = cx.window_id();
-                                        cx.remove_window(window_id)
-                                    },
-                                ),
-                                SignInContents::Enabled => theme::ui::cta_button_with_click(
-                                    "Done",
-                                    style.auth.content_width,
-                                    &style.auth.cta_button,
-                                    cx,
-                                    |_, cx| {
-                                        let window_id = cx.window_id();
-                                        cx.remove_window(window_id)
-                                    },
-                                ),
-                            }
-                        })
+                                    .boxed(),
+                                ])
+                                .align_children_center()
+                                .contained()
+                                .with_style(style.auth.enable_group.clone())
+                                .boxed(),
+                        ])
+                        .align_children_center()
+                        .contained()
+                        .with_style(style.auth.header_group)
+                        .aligned()
+                        .boxed(),
+                    Self::render_not_authorized_warning(&style),
+                    Flex::column()
+                        .with_child(theme::ui::cta_button_with_click(
+                            "Close",
+                            style.auth.content_width,
+                            &style.auth.cta_button,
+                            cx,
+                            |_, cx| {
+                                let window_id = cx.window_id();
+                                cx.remove_window(window_id)
+                            },
+                        ))
                         .align_children_center()
                         .contained()
                         .with_style(style.auth.github_group)
@@ -446,3 +500,33 @@ impl View for CopilotCodeVerification {
         })
     }
 }
+
+impl Entity for CopilotCodeVerification {
+    type Event = ();
+}
+
+impl View for CopilotCodeVerification {
+    fn ui_name() -> &'static str {
+        "CopilotCodeVerification"
+    }
+
+    fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut gpui::ViewContext<Self>) {
+        cx.notify()
+    }
+
+    fn focus_out(&mut self, _: gpui::AnyViewHandle, cx: &mut gpui::ViewContext<Self>) {
+        cx.notify()
+    }
+
+    fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox {
+        let style = cx.global::<Settings>().theme.copilot.clone();
+        match &self.status {
+            Status::SigningIn {
+                prompt: Some(prompt),
+            } => Self::render_prompting_modal(&prompt, &style, cx),
+            Status::Unauthorized => Self::render_unauthorized_modal(&style, cx),
+            Status::Authorized => Self::render_enabled_modal(&style, cx),
+            _ => Empty::new().boxed(),
+        }
+    }
+}