use std::{sync::Arc, time::Duration}; use crate::{onboarding_event, ZED_PREDICT_DATA_COLLECTION_CHOICE};use anyhow::Context as _;use client::{Client, UserStore};use db::kvp::KEY_VALUE_STORE;use fs::Fs;use gpui::{ ease_in_out, svg, Animation, AnimationExt as _, ClickEvent, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, MouseDownEvent, Render,};use language::language_settings::{AllLanguageSettings, EditPredictionProvider};use settings::{update_settings_file, Settings};use ui::{prelude::*, Checkbox, TintColor};use util::ResultExt;use workspace::{notifications::NotifyTaskExt, ModalView, Workspace}; /// Introduces user to Zed's Edit Prediction feature and terms of servicepub struct ZedPredictModal { user_store: Entity<UserStore>, client: Arc<Client>, fs: Arc<dyn Fs>, focus_handle: FocusHandle, sign_in_status: SignInStatus, terms_of_service: bool, data_collection_expanded: bool, data_collection_opted_in: bool,} #[derive(PartialEq, Eq)]enum SignInStatus { /// Signed out or signed in but not from this modal Idle, /// Authentication triggered from this modal Waiting, /// Signed in after authentication from this modal SignedIn,} impl ZedPredictModal { pub fn toggle( workspace: &mut Workspace, user_store: Entity<UserStore>, client: Arc<Client>, fs: Arc<dyn Fs>, window: &mut Window, cx: &mut Context<Workspace>, ) { workspace.toggle_modal(window, cx, |_window, cx| Self { user_store, client, fs, focus_handle: cx.focus_handle(), sign_in_status: SignInStatus::Idle, terms_of_service: false, data_collection_expanded: false, data_collection_opted_in: false, }); } fn view_terms(&mut self, _: &ClickEvent, _: &mut Window, cx: &mut Context<Self>) { cx.open_url("https://zed.dev/terms-of-service"); cx.notify(); onboarding_event!("ToS Link Clicked"); } fn view_blog(&mut self, _: &ClickEvent, _: &mut Window, cx: &mut Context<Self>) { cx.open_url("https://zed.dev/blog/edit-prediction"); cx.notify(); onboarding_event!("Blog Link clicked"); } fn inline_completions_doc(&mut self, _: &ClickEvent, _: &mut Window, cx: &mut Context<Self>) { cx.open_url("https://zed.dev/docs/configuring-zed#disabled-globs"); cx.notify(); onboarding_event!("Docs Link Clicked"); } fn accept_and_enable(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) { let task = self .user_store .update(cx, |this, cx| this.accept_terms_of_service(cx)); let fs = self.fs.clone(); cx.spawn(|this, mut cx| async move { task.await?; let mut data_collection_opted_in = false; this.update(&mut cx, |this, _cx| { data_collection_opted_in = this.data_collection_opted_in; }) .ok(); KEY_VALUE_STORE .write_kvp( ZED_PREDICT_DATA_COLLECTION_CHOICE.into(), data_collection_opted_in.to_string(), ) .await .log_err(); // Make sure edit prediction provider setting is using the new key let settings_path = paths::settings_file().as_path(); let settings_path = fs.canonicalize(settings_path).await.with_context(|| { format!("Failed to canonicalize settings path {:?}", settings_path) })?; if let Some(settings) = fs.load(&settings_path).await.log_err() { if let Some(new_settings) = migrator::migrate_edit_prediction_provider_settings(&settings)? { fs.atomic_write(settings_path, new_settings).await?; } } this.update(&mut cx, |this, cx| { update_settings_file::<AllLanguageSettings>(this.fs.clone(), cx, move |file, _| { file.features .get_or_insert(Default::default()) .edit_prediction_provider = Some(EditPredictionProvider::Zed); }); cx.emit(DismissEvent); }) }) .detach_and_notify_err(window, cx); onboarding_event!( "Enable Clicked", data_collection_opted_in = self.data_collection_opted_in, ); } fn sign_in(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) { let client = self.client.clone(); self.sign_in_status = SignInStatus::Waiting; cx.spawn(move |this, mut cx| async move { let result = client.authenticate_and_connect(true, &cx).await; let status = match result { Ok(_) => SignInStatus::SignedIn, Err(_) => SignInStatus::Idle, }; this.update(&mut cx, |this, cx| { this.sign_in_status = status; onboarding_event!("Signed In"); cx.notify() })?; result }) .detach_and_notify_err(window, cx); onboarding_event!("Sign In Clicked"); } fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) { cx.emit(DismissEvent); }} impl EventEmitter<DismissEvent> for ZedPredictModal {} impl Focusable for ZedPredictModal { fn focus_handle(&self, _cx: &App) -> FocusHandle { self.focus_handle.clone() }} impl ModalView for ZedPredictModal {} impl Render for ZedPredictModal { fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { let window_height = window.viewport_size().height; let max_height = window_height - px(200.); let base = v_flex() .id("edit-prediction-onboarding") .key_context("ZedPredictModal") .relative() .w(px(550.)) .h_full() .max_h(max_height) .p_4() .gap_2() .when(self.data_collection_expanded, |element| { element.overflow_y_scroll() }) .when(!self.data_collection_expanded, |element| { element.overflow_hidden() }) .elevation_3(cx) .track_focus(&self.focus_handle(cx)) .on_action(cx.listener(Self::cancel)) .on_action(cx.listener(|_, _: &menu::Cancel, _window, cx| { onboarding_event!("Cancelled", trigger = "Action"); cx.emit(DismissEvent); })) .on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, _cx| { this.focus_handle.focus(window); })) .child( div() .p_1p5() .absolute() .top_1() .left_1() .right_0() .h(px(200.)) .child( svg() .path("icons/zed_predict_bg.svg") .text_color(cx.theme().colors().icon_disabled) .w(px(530.)) .h(px(128.)) .overflow_hidden(), ), ) .child( h_flex() .w_full() .mb_2() .justify_between() .child( v_flex() .gap_1() .child( Label::new("Introducing Zed AI's") .size(LabelSize::Small) .color(Color::Muted), ) .child(Headline::new("Edit Prediction").size(HeadlineSize::Large)), ) .child({ let tab = |n: usize| { let text_color = cx.theme().colors().text; let border_color = cx.theme().colors().text_accent.opacity(0.4); h_flex().child( h_flex() .px_4() .py_0p5() .bg(cx.theme().colors().editor_background) .border_1() .border_color(border_color) .rounded_md() .font(theme::ThemeSettings::get_global(cx).buffer_font.clone()) .text_size(TextSize::XSmall.rems(cx)) .text_color(text_color) .child("tab") .with_animation( ElementId::Integer(n), Animation::new(Duration::from_secs(2)).repeat(), move |tab, delta| { let delta = (delta - 0.15 * n as f32) / 0.7; let delta = 1.0 - (0.5 - delta).abs() * 2.; let delta = ease_in_out(delta.clamp(0., 1.)); let delta = 0.1 + 0.9 * delta; tab.border_color(border_color.opacity(delta)) .text_color(text_color.opacity(delta)) }, ), ) }; v_flex() .gap_2() .items_center() .pr_2p5() .child(tab(0).ml_neg_20()) .child(tab(1)) .child(tab(2).ml_20()) }), ) .child(h_flex().absolute().top_2().right_2().child( IconButton::new("cancel", IconName::X).on_click(cx.listener( |_, _: &ClickEvent, _window, cx| { onboarding_event!("Cancelled", trigger = "X click"); cx.emit(DismissEvent); }, )), )); let blog_post_button = Button::new("view-blog", "Read the Blog Post") .full_width() .icon(IconName::ArrowUpRight) .icon_size(IconSize::Indicator) .icon_color(Color::Muted) .on_click(cx.listener(Self::view_blog)); if self.user_store.read(cx).current_user().is_some() { let copy = match self.sign_in_status { SignInStatus::Idle => { "Zed can now predict your next edit on every keystroke. Powered by Zeta, our open-source, open-dataset language model." } SignInStatus::SignedIn => "Almost there! Ensure you:", SignInStatus::Waiting => unreachable!(), }; let accordion_icons = if self.data_collection_expanded { (IconName::ChevronUp, IconName::ChevronDown) } else { (IconName::ChevronDown, IconName::ChevronUp) }; fn label_item(label_text: impl Into<SharedString>) -> impl Element { Label::new(label_text).color(Color::Muted).into_element() } fn info_item(label_text: impl Into<SharedString>) -> impl Element { h_flex() .items_start() .gap_2() .child( div() .mt_1p5() .child(Icon::new(IconName::Check).size(IconSize::XSmall)), ) .child(div().w_full().child(label_item(label_text))) } fn multiline_info_item<E1: Into<SharedString>, E2: IntoElement>( first_line: E1, second_line: E2, ) -> impl Element { v_flex() .child(info_item(first_line)) .child(div().pl_5().child(second_line)) } base.child(Label::new(copy).color(Color::Muted)) .child( h_flex() .child( Checkbox::new("tos-checkbox", self.terms_of_service.into()) .fill() .label("I have read and accept the") .on_click(cx.listener(move |this, state, _window, cx| { this.terms_of_service = *state == ToggleState::Selected; cx.notify(); })), ) .child( Button::new("view-tos", "Terms of Service") .icon(IconName::ArrowUpRight) .icon_size(IconSize::Indicator) .icon_color(Color::Muted) .on_click(cx.listener(Self::view_terms)), ), ) .child( v_flex() .child( h_flex() .flex_wrap() .child( Checkbox::new( "training-data-checkbox", self.data_collection_opted_in.into(), ) .label("Contribute to the open dataset when editing open source.") .fill() .on_click(cx.listener( move |this, state, _window, cx| { this.data_collection_opted_in = *state == ToggleState::Selected; cx.notify() }, )), ) .child( Button::new("learn-more", "Learn More") .icon(accordion_icons.0) .icon_size(IconSize::Indicator) .icon_color(Color::Muted) .on_click(cx.listener(|this, _, _, cx| { this.data_collection_expanded = !this.data_collection_expanded; cx.notify(); if this.data_collection_expanded { onboarding_event!("Data Collection Learn More Clicked"); } })), ), ) .when(self.data_collection_expanded, |element| { element.child( v_flex() .mt_2() .p_2() .rounded_md() .bg(cx.theme().colors().editor_background.opacity(0.5)) .border_1() .border_color(cx.theme().colors().border_variant) .child( div().child( Label::new("To improve edit predictions, please consider contributing to our open dataset based on your interactions within open source repositories.") .mb_1() ) ) .child(info_item( "We collect data exclusively from open source projects.", )) .child(info_item( "Zed automatically detects if your project is open source.", )) .child(info_item("Toggle participation at any time via the status bar menu.")) .child(multiline_info_item( "If turned on, this setting applies for all open source repositories", label_item("you open in Zed.") )) .child(multiline_info_item( "Files with sensitive data, like `.env`, are excluded by default", h_flex() .w_full() .flex_wrap() .child(label_item("via the")) .child( Button::new("doc-link", "disabled_globs").on_click( cx.listener(Self::inline_completions_doc), ), ) .child(label_item("setting.")), )), ) }), ) .child( v_flex() .mt_2() .gap_2() .w_full() .child( Button::new("accept-tos", "Enable Edit Prediction") .disabled(!self.terms_of_service) .style(ButtonStyle::Tinted(TintColor::Accent)) .full_width() .on_click(cx.listener(Self::accept_and_enable)), ) .child(blog_post_button), ) } else { base.child( Label::new("To set Zed as your edit prediction provider, please sign in.") .color(Color::Muted), ) .child( v_flex() .mt_2() .gap_2() .w_full() .child( Button::new("accept-tos", "Sign in with GitHub") .disabled(self.sign_in_status == SignInStatus::Waiting) .style(ButtonStyle::Tinted(TintColor::Accent)) .full_width() .on_click(cx.listener(Self::sign_in)), ) .child(blog_post_button), ) } }}
use std::{sync::Arc, time::Duration};
use crate::{onboarding_event, ZED_PREDICT_DATA_COLLECTION_CHOICE};
use anyhow::Context as _;
use client::{Client, UserStore};
use db::kvp::KEY_VALUE_STORE;
use fs::Fs;
use gpui::{
ease_in_out, svg, Animation, AnimationExt as _, ClickEvent, DismissEvent, Entity, EventEmitter,
FocusHandle, Focusable, MouseDownEvent, Render,
};
use language::language_settings::{AllLanguageSettings, EditPredictionProvider};
use settings::{update_settings_file, Settings};
use ui::{prelude::*, Checkbox, TintColor};
use util::ResultExt;
use workspace::{notifications::NotifyTaskExt, ModalView, Workspace};
/// Introduces user to Zed's Edit Prediction feature and terms of service
pub struct ZedPredictModal {
user_store: Entity<UserStore>,
client: Arc<Client>,
fs: Arc<dyn Fs>,
focus_handle: FocusHandle,
sign_in_status: SignInStatus,
terms_of_service: bool,
data_collection_expanded: bool,
data_collection_opted_in: bool,
}
#[derive(PartialEq, Eq)]
enum SignInStatus {
/// Signed out or signed in but not from this modal
Idle,
/// Authentication triggered from this modal
Waiting,
/// Signed in after authentication from this modal
SignedIn,
}
impl ZedPredictModal {
pub fn toggle(
workspace: &mut Workspace,
user_store: Entity<UserStore>,
client: Arc<Client>,
fs: Arc<dyn Fs>,
window: &mut Window,
cx: &mut Context<Workspace>,
) {
workspace.toggle_modal(window, cx, |_window, cx| Self {
user_store,
client,
fs,
focus_handle: cx.focus_handle(),
sign_in_status: SignInStatus::Idle,
terms_of_service: false,
data_collection_expanded: false,
data_collection_opted_in: false,
});
}
fn view_terms(&mut self, _: &ClickEvent, _: &mut Window, cx: &mut Context<Self>) {
cx.open_url("https://zed.dev/terms-of-service");
cx.notify();
onboarding_event!("ToS Link Clicked");
}
fn view_blog(&mut self, _: &ClickEvent, _: &mut Window, cx: &mut Context<Self>) {
cx.open_url("https://zed.dev/blog/edit-prediction");
cx.notify();
onboarding_event!("Blog Link clicked");
}
fn inline_completions_doc(&mut self, _: &ClickEvent, _: &mut Window, cx: &mut Context<Self>) {
cx.open_url("https://zed.dev/docs/configuring-zed#disabled-globs");
cx.notify();
onboarding_event!("Docs Link Clicked");
}
fn accept_and_enable(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
let task = self
.user_store
.update(cx, |this, cx| this.accept_terms_of_service(cx));
let fs = self.fs.clone();
cx.spawn(|this, mut cx| async move {
task.await?;
let mut data_collection_opted_in = false;
this.update(&mut cx, |this, _cx| {
data_collection_opted_in = this.data_collection_opted_in;
})
.ok();
KEY_VALUE_STORE
.write_kvp(
ZED_PREDICT_DATA_COLLECTION_CHOICE.into(),
data_collection_opted_in.to_string(),
)
.await
.log_err();
// Make sure edit prediction provider setting is using the new key
let settings_path = paths::settings_file().as_path();
let settings_path = fs.canonicalize(settings_path).await.with_context(|| {
format!("Failed to canonicalize settings path {:?}", settings_path)
})?;
if let Some(settings) = fs.load(&settings_path).await.log_err() {
if let Some(new_settings) =
migrator::migrate_edit_prediction_provider_settings(&settings)?
{
fs.atomic_write(settings_path, new_settings).await?;
}
}
this.update(&mut cx, |this, cx| {
update_settings_file::<AllLanguageSettings>(this.fs.clone(), cx, move |file, _| {
file.features
.get_or_insert(Default::default())
.edit_prediction_provider = Some(EditPredictionProvider::Zed);
});
cx.emit(DismissEvent);
})
})
.detach_and_notify_err(window, cx);
onboarding_event!(
"Enable Clicked",
data_collection_opted_in = self.data_collection_opted_in,
);
}
fn sign_in(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
let client = self.client.clone();
self.sign_in_status = SignInStatus::Waiting;
cx.spawn(move |this, mut cx| async move {
let result = client.authenticate_and_connect(true, &cx).await;
let status = match result {
Ok(_) => SignInStatus::SignedIn,
Err(_) => SignInStatus::Idle,
};
this.update(&mut cx, |this, cx| {
this.sign_in_status = status;
onboarding_event!("Signed In");
cx.notify()
})?;
result
})
.detach_and_notify_err(window, cx);
onboarding_event!("Sign In Clicked");
}
fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
cx.emit(DismissEvent);
}
}
impl EventEmitter<DismissEvent> for ZedPredictModal {}
impl Focusable for ZedPredictModal {
fn focus_handle(&self, _cx: &App) -> FocusHandle {
self.focus_handle.clone()
}
}
impl ModalView for ZedPredictModal {}
impl Render for ZedPredictModal {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let window_height = window.viewport_size().height;
let max_height = window_height - px(200.);
let base = v_flex()
.id("edit-prediction-onboarding")
.key_context("ZedPredictModal")
.relative()
.w(px(550.))
.h_full()
.max_h(max_height)
.p_4()
.gap_2()
.when(self.data_collection_expanded, |element| {
element.overflow_y_scroll()
})
.when(!self.data_collection_expanded, |element| {
element.overflow_hidden()
})
.elevation_3(cx)
.track_focus(&self.focus_handle(cx))
.on_action(cx.listener(Self::cancel))
.on_action(cx.listener(|_, _: &menu::Cancel, _window, cx| {
onboarding_event!("Cancelled", trigger = "Action");
cx.emit(DismissEvent);
}))
.on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, _cx| {
this.focus_handle.focus(window);
}))
.child(
div()
.p_1p5()
.absolute()
.top_1()
.left_1()
.right_0()
.h(px(200.))
.child(
svg()
.path("icons/zed_predict_bg.svg")
.text_color(cx.theme().colors().icon_disabled)
.w(px(530.))
.h(px(128.))
.overflow_hidden(),
),
)
.child(
h_flex()
.w_full()
.mb_2()
.justify_between()
.child(
v_flex()
.gap_1()
.child(
Label::new("Introducing Zed AI's")
.size(LabelSize::Small)
.color(Color::Muted),
)
.child(Headline::new("Edit Prediction").size(HeadlineSize::Large)),
)
.child({
let tab = |n: usize| {
let text_color = cx.theme().colors().text;
let border_color = cx.theme().colors().text_accent.opacity(0.4);
h_flex().child(
h_flex()
.px_4()
.py_0p5()
.bg(cx.theme().colors().editor_background)
.border_1()
.border_color(border_color)
.rounded_md()
.font(theme::ThemeSettings::get_global(cx).buffer_font.clone())
.text_size(TextSize::XSmall.rems(cx))
.text_color(text_color)
.child("tab")
.with_animation(
ElementId::Integer(n),
Animation::new(Duration::from_secs(2)).repeat(),
move |tab, delta| {
let delta = (delta - 0.15 * n as f32) / 0.7;
let delta = 1.0 - (0.5 - delta).abs() * 2.;
let delta = ease_in_out(delta.clamp(0., 1.));
let delta = 0.1 + 0.9 * delta;
tab.border_color(border_color.opacity(delta))
.text_color(text_color.opacity(delta))
},
),
)
};
v_flex()
.gap_2()
.items_center()
.pr_2p5()
.child(tab(0).ml_neg_20())
.child(tab(1))
.child(tab(2).ml_20())
}),
)
.child(h_flex().absolute().top_2().right_2().child(
IconButton::new("cancel", IconName::X).on_click(cx.listener(
|_, _: &ClickEvent, _window, cx| {
onboarding_event!("Cancelled", trigger = "X click");
cx.emit(DismissEvent);
},
)),
));
let blog_post_button = Button::new("view-blog", "Read the Blog Post")
.full_width()
.icon(IconName::ArrowUpRight)
.icon_size(IconSize::Indicator)
.icon_color(Color::Muted)
.on_click(cx.listener(Self::view_blog));
if self.user_store.read(cx).current_user().is_some() {
let copy = match self.sign_in_status {
SignInStatus::Idle => {
"Zed can now predict your next edit on every keystroke. Powered by Zeta, our open-source, open-dataset language model."
}
SignInStatus::SignedIn => "Almost there! Ensure you:",
SignInStatus::Waiting => unreachable!(),
};
let accordion_icons = if self.data_collection_expanded {
(IconName::ChevronUp, IconName::ChevronDown)
} else {
(IconName::ChevronDown, IconName::ChevronUp)
};
fn label_item(label_text: impl Into<SharedString>) -> impl Element {
Label::new(label_text).color(Color::Muted).into_element()
}
fn info_item(label_text: impl Into<SharedString>) -> impl Element {
h_flex()
.items_start()
.gap_2()
.child(
div()
.mt_1p5()
.child(Icon::new(IconName::Check).size(IconSize::XSmall)),
)
.child(div().w_full().child(label_item(label_text)))
}
fn multiline_info_item<E1: Into<SharedString>, E2: IntoElement>(
first_line: E1,
second_line: E2,
) -> impl Element {
v_flex()
.child(info_item(first_line))
.child(div().pl_5().child(second_line))
}
base.child(Label::new(copy).color(Color::Muted))
.child(
h_flex()
.child(
Checkbox::new("tos-checkbox", self.terms_of_service.into())
.fill()
.label("I have read and accept the")
.on_click(cx.listener(move |this, state, _window, cx| {
this.terms_of_service = *state == ToggleState::Selected;
cx.notify();
})),
)
.child(
Button::new("view-tos", "Terms of Service")
.icon(IconName::ArrowUpRight)
.icon_size(IconSize::Indicator)
.icon_color(Color::Muted)
.on_click(cx.listener(Self::view_terms)),
),
)
.child(
v_flex()
.child(
h_flex()
.flex_wrap()
.child(
Checkbox::new(
"training-data-checkbox",
self.data_collection_opted_in.into(),
)
.label("Contribute to the open dataset when editing open source.")
.fill()
.on_click(cx.listener(
move |this, state, _window, cx| {
this.data_collection_opted_in =
*state == ToggleState::Selected;
cx.notify()
},
)),
)
.child(
Button::new("learn-more", "Learn More")
.icon(accordion_icons.0)
.icon_size(IconSize::Indicator)
.icon_color(Color::Muted)
.on_click(cx.listener(|this, _, _, cx| {
this.data_collection_expanded =
!this.data_collection_expanded;
cx.notify();
if this.data_collection_expanded {
onboarding_event!("Data Collection Learn More Clicked");
}
})),
),
)
.when(self.data_collection_expanded, |element| {
element.child(
v_flex()
.mt_2()
.p_2()
.rounded_md()
.bg(cx.theme().colors().editor_background.opacity(0.5))
.border_1()
.border_color(cx.theme().colors().border_variant)
.child(
div().child(
Label::new("To improve edit predictions, please consider contributing to our open dataset based on your interactions within open source repositories.")
.mb_1()
)
)
.child(info_item(
"We collect data exclusively from open source projects.",
))
.child(info_item(
"Zed automatically detects if your project is open source.",
))
.child(info_item("Toggle participation at any time via the status bar menu."))
.child(multiline_info_item(
"If turned on, this setting applies for all open source repositories",
label_item("you open in Zed.")
))
.child(multiline_info_item(
"Files with sensitive data, like `.env`, are excluded by default",
h_flex()
.w_full()
.flex_wrap()
.child(label_item("via the"))
.child(
Button::new("doc-link", "disabled_globs").on_click(
cx.listener(Self::inline_completions_doc),
),
)
.child(label_item("setting.")),
)),
)
}),
)
.child(
v_flex()
.mt_2()
.gap_2()
.w_full()
.child(
Button::new("accept-tos", "Enable Edit Prediction")
.disabled(!self.terms_of_service)
.style(ButtonStyle::Tinted(TintColor::Accent))
.full_width()
.on_click(cx.listener(Self::accept_and_enable)),
)
.child(blog_post_button),
)
} else {
base.child(
Label::new("To set Zed as your edit prediction provider, please sign in.")
.color(Color::Muted),
)
.child(
v_flex()
.mt_2()
.gap_2()
.w_full()
.child(
Button::new("accept-tos", "Sign in with GitHub")
.disabled(self.sign_in_status == SignInStatus::Waiting)
.style(ButtonStyle::Tinted(TintColor::Accent))
.full_width()
.on_click(cx.listener(Self::sign_in)),
)
.child(blog_post_button),
)
}
}
}