diff --git a/crates/ely_app/src/shell/internal_pages.rs b/crates/ely_app/src/shell/internal_pages.rs index 4e9e204..41668e9 100644 --- a/crates/ely_app/src/shell/internal_pages.rs +++ b/crates/ely_app/src/shell/internal_pages.rs @@ -32,6 +32,7 @@ mod sleep; mod space_actions; mod spaces; mod sync; +mod sync_controls; mod tab_context; mod task_manager; mod updates; diff --git a/crates/ely_app/src/shell/internal_pages/sync.rs b/crates/ely_app/src/shell/internal_pages/sync.rs index 056c92c..bed3209 100644 --- a/crates/ely_app/src/shell/internal_pages/sync.rs +++ b/crates/ely_app/src/shell/internal_pages/sync.rs @@ -1,11 +1,8 @@ use ely_browser_core::BrowserSnapshot; use ely_design_system::colors; -use ely_domain::{ - SyncConnectionState, SyncObjectKind, SyncObjectPolicy, SyncObjectState, SyncObjectStatus, -}; +use ely_domain::{SyncConnectionState, SyncObjectKind, SyncObjectState, SyncObjectStatus}; use gpui::{ - AnyElement, Context, FontWeight, InteractiveElement, IntoElement, ParentElement, SharedString, - StatefulInteractiveElement, Styled, div, prelude::FluentBuilder, px, rgb, rgba, + AnyElement, Context, FontWeight, IntoElement, ParentElement, Styled, div, px, rgb, rgba, }; use gpui_component::{IconName, input::Input, scroll::ScrollableElement}; @@ -13,6 +10,10 @@ use crate::shell::auth::AuthFlowPhase; use crate::brand::SYNC_SERVICE_NAME; +use super::sync_controls::{ + button_bg, render_dual_button_row, render_policy_toggle, render_primary_button, + render_reset_button, render_sign_out_button, +}; use super::{ElyShell, render_canvas_surface}; use crate::shell::chrome::SERIF_FAMILY; @@ -30,7 +31,7 @@ impl ElyShell { .grid_cols(2) .gap(px(32.0)) .child(render_left_column(self, snapshot, cx)) - .child(render_right_column(snapshot, cx)), + .child(render_right_column(self, snapshot, cx)), ), ) } @@ -50,7 +51,7 @@ fn render_left_column( .child(render_serif_headline()) .child(render_intro_paragraph()) .child(render_account_card(shell, snapshot, cx)) - .child(render_metrics_card(snapshot, cx)) + .child(render_metrics_card(shell, snapshot, cx)) .into_any_element() } @@ -95,7 +96,11 @@ fn render_intro_paragraph() -> AnyElement { .into_any_element() } -fn render_metrics_card(snapshot: &BrowserSnapshot, cx: &mut Context) -> AnyElement { +fn render_metrics_card( + shell: &ElyShell, + snapshot: &BrowserSnapshot, + cx: &mut Context, +) -> AnyElement { div() .max_w(px(380.0)) .p(px(20.0)) @@ -121,7 +126,7 @@ fn render_metrics_card(snapshot: &BrowserSnapshot, cx: &mut Context) - colors::error(), )), ) - .child(render_reset_button(cx)) + .child(render_reset_button(shell, cx)) .into_any_element() } @@ -151,7 +156,7 @@ fn render_account_card( | SyncConnectionState::SyncError { .. } => card .child(render_account_heading("Account")) .child(render_signed_in_chip()) - .child(render_sign_out_button(cx)) + .child(render_sign_out_button(shell, cx)) .into_any_element(), } } @@ -168,6 +173,7 @@ fn account_form(shell: &ElyShell, cx: &mut Context) -> Vec match &phase { AuthFlowPhase::Idle | AuthFlowPhase::Error { .. } => { elements.push(render_primary_button( + shell, "send-otp", "Send code", false, @@ -178,12 +184,20 @@ fn account_form(shell: &ElyShell, cx: &mut Context) -> Vec )); } AuthFlowPhase::SendingCode { .. } => { - elements.push(render_primary_button("send-otp", "Sending…", true, cx, |_, _| {})); + elements.push(render_primary_button( + shell, + "send-otp", + "Sending...", + true, + cx, + |_, _| {}, + )); } AuthFlowPhase::AwaitingOtp { .. } | AuthFlowPhase::Verifying { .. } => { elements.push(render_account_label("Code")); elements.push(render_input(&shell.auth_otp_input, None)); elements.push(render_dual_button_row( + shell, phase.is_busy(), cx, |shell, cx| shell.submit_email_otp_verify(cx), @@ -243,77 +257,6 @@ fn render_input( wrapper.into_any_element() } -fn render_primary_button( - id: &'static str, - label: &'static str, - disabled: bool, - cx: &mut Context, - handler: F, -) -> AnyElement -where - F: Fn(&mut ElyShell, &mut Context) + 'static, -{ - div() - .id(SharedString::from(id)) - .px(px(14.0)) - .py(px(8.0)) - .rounded(px(8.0)) - .bg(rgba(colors::accent())) - .text_size(px(12.5)) - .font_weight(FontWeight(500.0)) - .text_color(rgb(0xfff5e6)) - .when(!disabled, |el| { - el.cursor_pointer() - .hover(|style| style.opacity(0.92)) - .active(|style| style.opacity(0.78)) - .on_click(cx.listener(move |shell, _, _, cx| handler(shell, cx))) - }) - .when(disabled, |el| el.opacity(0.6)) - .child(label) - .into_any_element() -} - -fn render_dual_button_row( - disabled: bool, - cx: &mut Context, - primary: P, - secondary: S, -) -> AnyElement -where - P: Fn(&mut ElyShell, &mut Context) + 'static, - S: Fn(&mut ElyShell, &mut Context) + 'static, -{ - div() - .flex() - .gap(px(8.0)) - .child(render_primary_button( - "verify-otp", - if disabled { "Verifying…" } else { "Verify" }, - disabled, - cx, - primary, - )) - .child( - div() - .id(SharedString::from("resend-otp")) - .px(px(12.0)) - .py(px(8.0)) - .rounded(px(8.0)) - .bg(rgba(button_bg())) - .text_size(px(12.0)) - .text_color(rgb(colors::ink_2())) - .when(!disabled, |el| { - el.cursor_pointer() - .hover(|style| style.bg(rgba(button_bg_hover()))) - .active(|style| style.opacity(0.85)) - .on_click(cx.listener(move |shell, _, _, cx| secondary(shell, cx))) - }) - .when(disabled, |el| el.opacity(0.6)) - .child("Resend code"), - ) - .into_any_element() -} - fn render_inline_error(message: &str) -> AnyElement { div() .text_size(px(11.5)) @@ -330,24 +273,6 @@ fn render_signed_in_chip() -> AnyElement { .into_any_element() } -fn render_sign_out_button(cx: &mut Context) -> AnyElement { - div() - .id(SharedString::from("sign-out")) - .px(px(12.0)) - .py(px(7.0)) - .rounded(px(8.0)) - .bg(rgba(button_bg())) - .text_size(px(12.0)) - .font_weight(FontWeight(500.0)) - .text_color(rgb(colors::ink_2())) - .cursor_pointer() - .hover(|style| style.bg(rgba(button_bg_hover()))) - .active(|style| style.opacity(0.85)) - .on_click(cx.listener(|shell, _, _, cx| shell.submit_sign_out(cx))) - .child("Sign out") - .into_any_element() -} - fn render_metric(label: &'static str, value: usize, color: u32) -> AnyElement { div() .flex() @@ -364,55 +289,24 @@ fn render_metric(label: &'static str, value: usize, color: u32) -> AnyElement { .into_any_element() } -fn render_reset_button(cx: &mut Context) -> AnyElement { - div() - .flex() - .gap(px(8.0)) - .child( - div() - .id(SharedString::from("sync-upload")) - .px(px(12.0)) - .py(px(7.0)) - .rounded(px(8.0)) - .bg(rgba(colors::accent())) - .text_size(px(12.0)) - .font_weight(FontWeight(500.0)) - .text_color(rgb(0xfff5e6)) - .cursor_pointer() - .hover(|style| style.opacity(0.92)) - .active(|style| style.opacity(0.78)) - .on_click(cx.listener(|shell, _, _, _| shell.trigger_cloud_sync_upload())) - .child("Sync now"), - ) - .child( - div() - .id(SharedString::from("sync-reset")) - .px(px(12.0)) - .py(px(7.0)) - .rounded(px(8.0)) - .bg(rgba(button_bg())) - .text_size(px(12.0)) - .font_weight(FontWeight(500.0)) - .text_color(rgb(colors::ink_2())) - .cursor_pointer() - .hover(|style| style.bg(rgba(button_bg_hover()))) - .active(|style| style.opacity(0.85)) - .on_click(cx.listener(|shell, _, _, cx| shell.reset_sync_settings(cx))) - .child("Reset to defaults"), - ) - .into_any_element() -} - -fn render_right_column(snapshot: &BrowserSnapshot, cx: &mut Context) -> AnyElement { +fn render_right_column( + shell: &ElyShell, + snapshot: &BrowserSnapshot, + cx: &mut Context, +) -> AnyElement { div() .flex() .flex_col() .gap(px(14.0)) - .child(render_what_syncs_card(snapshot, cx)) + .child(render_what_syncs_card(shell, snapshot, cx)) .into_any_element() } -fn render_what_syncs_card(snapshot: &BrowserSnapshot, cx: &mut Context) -> AnyElement { +fn render_what_syncs_card( + shell: &ElyShell, + snapshot: &BrowserSnapshot, + cx: &mut Context, +) -> AnyElement { div() .p(px(18.0)) .rounded(px(16.0)) @@ -448,13 +342,14 @@ fn render_what_syncs_card(snapshot: &BrowserSnapshot, cx: &mut Context .objects() .iter() .enumerate() - .map(|(index, status)| render_sync_object_row(index, status, cx)), + .map(|(index, status)| render_sync_object_row(shell, index, status, cx)), ), ) .into_any_element() } fn render_sync_object_row( + shell: &ElyShell, index: usize, status: &SyncObjectStatus, cx: &mut Context, @@ -487,7 +382,7 @@ fn render_sync_object_row( sync_object_state_label(status.state()) ))), ) - .child(render_policy_toggle(index, status, cx)) + .child(render_policy_toggle(shell, index, status, cx)) .into_any_element() } @@ -502,39 +397,6 @@ fn render_state_dot(state: SyncObjectState) -> AnyElement { div().size(px(8.0)).rounded_full().bg(rgb(color)).into_any_element() } -fn render_policy_toggle( - index: usize, - status: &SyncObjectStatus, - cx: &mut Context, -) -> AnyElement { - let enabled = status.policy() == SyncObjectPolicy::Enabled; - let next_policy = if enabled { SyncObjectPolicy::Paused } else { SyncObjectPolicy::Enabled }; - let kind = status.kind(); - let track_color = if enabled { colors::accent() } else { 0x281e1426 }; - - div() - .id(SharedString::from(format!("sync-policy-{index}"))) - .w(px(34.0)) - .h(px(20.0)) - .rounded_full() - .bg(rgba(track_color)) - .p(px(2.0)) - .cursor_pointer() - .hover(|style| style.opacity(0.9)) - .active(|style| style.opacity(0.78)) - .on_click(cx.listener(move |shell, _, _, cx| { - shell.set_sync_object_policy(kind, next_policy, cx); - })) - .child( - div() - .size(px(16.0)) - .rounded_full() - .bg(rgb(0xffffff)) - .when(enabled, |this| this.ml(px(14.0))), - ) - .into_any_element() -} - fn connection_label(connection: &SyncConnectionState) -> String { match connection { SyncConnectionState::SignedOut => "Local-only · drop a session token to enable".to_string(), @@ -606,9 +468,3 @@ fn pill_bg() -> u32 { fn card_bg() -> u32 { colors::pick(0xffffffd9, 0x1f1d1bd9) } -fn button_bg() -> u32 { - colors::pick(0xffffffd9, 0x1f1d1bd9) -} -fn button_bg_hover() -> u32 { - colors::pick(0xffffffeb, 0x1f1d1beb) -} diff --git a/crates/ely_app/src/shell/internal_pages/sync_controls.rs b/crates/ely_app/src/shell/internal_pages/sync_controls.rs new file mode 100644 index 0000000..6fc5de0 --- /dev/null +++ b/crates/ely_app/src/shell/internal_pages/sync_controls.rs @@ -0,0 +1,177 @@ +use ely_design_system::colors; +use ely_domain::{SyncObjectPolicy, SyncObjectStatus}; +use gpui::{ + AnyElement, Context, FontWeight, InteractiveElement, IntoElement, ParentElement, SharedString, + StatefulInteractiveElement, Styled, div, prelude::FluentBuilder, px, rgb, rgba, +}; + +use crate::shell::ElyShell; +use crate::shell::chrome::animations::chrome_motion_feedback; + +pub(super) fn render_primary_button( + shell: &ElyShell, + id: &'static str, + label: &'static str, + disabled: bool, + cx: &mut Context, + handler: F, +) -> AnyElement +where + F: Fn(&mut ElyShell, &mut Context) + 'static, +{ + let press_id = if disabled { None } else { shell.chrome_motion_animation_id(id) }; + let element = div() + .id(SharedString::from(id)) + .px(px(14.0)) + .py(px(8.0)) + .rounded(px(8.0)) + .bg(rgba(colors::accent())) + .text_size(px(12.5)) + .font_weight(FontWeight(500.0)) + .text_color(rgb(0xfff5e6)) + .when(!disabled, |el| { + el.cursor_pointer() + .hover(|style| style.opacity(0.92)) + .active(|style| style.opacity(0.78)) + .on_click(cx.listener(move |shell, _, _, cx| { + shell.trigger_chrome_motion(id); + handler(shell, cx); + })) + }) + .when(disabled, |el| el.opacity(0.6)) + .child(label); + + chrome_motion_feedback(press_id, SharedString::from(format!("{id}-selection")), false, element) +} + +pub(super) fn render_dual_button_row( + shell: &ElyShell, + disabled: bool, + cx: &mut Context, + primary: P, + secondary: S, +) -> AnyElement +where + P: Fn(&mut ElyShell, &mut Context) + 'static, + S: Fn(&mut ElyShell, &mut Context) + 'static, +{ + div() + .flex() + .gap(px(8.0)) + .child(render_primary_button( + shell, + "verify-otp", + if disabled { "Verifying..." } else { "Verify" }, + disabled, + cx, + primary, + )) + .child(render_secondary_button(shell, "resend-otp", "Resend code", disabled, cx, secondary)) + .into_any_element() +} + +pub(super) fn render_sign_out_button(shell: &ElyShell, cx: &mut Context) -> AnyElement { + render_secondary_button(shell, "sign-out", "Sign out", false, cx, |shell, cx| { + shell.submit_sign_out(cx); + }) +} + +pub(super) fn render_reset_button(shell: &ElyShell, cx: &mut Context) -> AnyElement { + div() + .flex() + .gap(px(8.0)) + .child(render_primary_button(shell, "sync-upload", "Sync now", false, cx, |shell, _| { + shell.trigger_cloud_sync_upload(); + })) + .child(render_secondary_button( + shell, + "sync-reset", + "Reset to defaults", + false, + cx, + |shell, cx| shell.reset_sync_settings(cx), + )) + .into_any_element() +} + +pub(super) fn render_policy_toggle( + shell: &ElyShell, + index: usize, + status: &SyncObjectStatus, + cx: &mut Context, +) -> AnyElement { + let enabled = status.policy() == SyncObjectPolicy::Enabled; + let next_policy = if enabled { SyncObjectPolicy::Paused } else { SyncObjectPolicy::Enabled }; + let kind = status.kind(); + let track_color = if enabled { colors::accent() } else { 0x281e1426 }; + let id = SharedString::from(format!("sync-policy-{index}")); + let press_id = shell.chrome_motion_animation_id(id.as_str()); + let selection_id = SharedString::from(format!("{}-selection", id.as_str())); + + let element = div() + .id(id.clone()) + .w(px(34.0)) + .h(px(20.0)) + .rounded_full() + .bg(rgba(track_color)) + .p(px(2.0)) + .cursor_pointer() + .hover(|style| style.opacity(0.9)) + .active(|style| style.opacity(0.78)) + .on_click(cx.listener(move |shell, _, _, cx| { + shell.trigger_chrome_motion(id.clone()); + shell.set_sync_object_policy(kind, next_policy, cx); + })) + .child( + div() + .size(px(16.0)) + .rounded_full() + .bg(rgb(0xffffff)) + .when(enabled, |this| this.ml(px(14.0))), + ); + + chrome_motion_feedback(press_id, selection_id, enabled, element) +} + +fn render_secondary_button( + shell: &ElyShell, + id: &'static str, + label: &'static str, + disabled: bool, + cx: &mut Context, + handler: F, +) -> AnyElement +where + F: Fn(&mut ElyShell, &mut Context) + 'static, +{ + let press_id = if disabled { None } else { shell.chrome_motion_animation_id(id) }; + let element = div() + .id(SharedString::from(id)) + .px(px(12.0)) + .py(px(8.0)) + .rounded(px(8.0)) + .bg(rgba(button_bg())) + .text_size(px(12.0)) + .text_color(rgb(colors::ink_2())) + .when(!disabled, |el| { + el.cursor_pointer() + .hover(|style| style.bg(rgba(button_bg_hover()))) + .active(|style| style.opacity(0.85)) + .on_click(cx.listener(move |shell, _, _, cx| { + shell.trigger_chrome_motion(id); + handler(shell, cx); + })) + }) + .when(disabled, |el| el.opacity(0.6)) + .child(label); + + chrome_motion_feedback(press_id, SharedString::from(format!("{id}-selection")), false, element) +} + +pub(super) fn button_bg() -> u32 { + colors::pick(0xffffffd9, 0x1f1d1bd9) +} + +fn button_bg_hover() -> u32 { + colors::pick(0xffffffeb, 0x1f1d1beb) +}