Files
galaxy/crates/onboarding/src/agent_onboarding_view.rs
T

807 lines
30 KiB
Rust

use std::time::Duration;
use ai::LLMId;
use galaxy_core::features::FeatureFlag;
use galaxy_core::send_telemetry_from_ctx;
use galaxyui_core::assets::asset_cache::AssetSource;
use galaxyui_core::image_cache::ImageType;
use galaxyui_core::windowing::state::{ApplicationStage, StateEvent};
use galaxyui_core::windowing::WindowManager;
use instant::Instant;
use crate::components::feature_optout_dialog::{render_feature_optout_dialog, FeatureOptOutDialog};
use crate::model::{
OnboardingAuthState, OnboardingStateEvent, OnboardingStateModel, OnboardingStep,
SelectedSettings,
};
use crate::slides::{
AgentSlide, AiAccessSlide, AiAccessSlideEvent, AiSetupSlide, CustomizeUISlide, IntentionSlide,
IntroSlide, IntroSlideEvent, OnboardingModelInfo, OnboardingSlide, ProjectSlide,
ThemePickerSlide, ThemePickerSlideEvent, ThirdPartySlide,
};
use crate::telemetry::OnboardingEvent;
const APP_BECAME_ACTIVE_DEBOUNCE: Duration = Duration::from_secs(15);
const PLAN_ACTIVATED_TOAST_DURATION: Duration = Duration::from_secs(5);
use galaxy_core::ui::appearance::Appearance;
use galaxy_core::ui::theme::{Fill, GalaxyTheme};
use galaxy_core::ui::Icon;
use galaxyui_core::elements::{
Align, CacheOption, ChildAnchor, ConstrainedBox, Container, CrossAxisAlignment, Dismiss, Empty,
Flex, Image, MainAxisAlignment, MainAxisSize, MouseStateHandle, OffsetPositioning,
ParentAnchor, ParentElement, ParentOffsetBounds, Rect, Shrinkable, Stack,
};
use galaxyui_core::fonts::Weight;
use galaxyui_core::keymap::macros::*;
use galaxyui_core::keymap::{FixedBinding, Keystroke};
use galaxyui_core::presenter::ChildView;
use galaxyui_core::ui_components::components::{UiComponent as _, UiComponentStyles};
use galaxyui_core::{
AppContext, Element, Entity, ModelHandle, SingletonEntity as _, TypedActionView, View,
ViewContext, ViewHandle,
};
use pathfinder_color::ColorU;
use pathfinder_geometry::vector::vec2f;
use ui_components::{button, Component as _, Options as _};
#[derive(Clone, Debug)]
pub enum AgentOnboardingEvent {
ThemeSelected {
theme_name: String,
},
SyncWithOsToggled {
enabled: bool,
},
OnboardingCompleted(SelectedSettings),
OnboardingSkipped,
LoginFromWelcomeRequested,
/// Emitted when the user clicks the "Privacy Settings" link on the terminal
/// intention theme slide. The variant name encodes that the event is only
/// emitted from the terminal-intention theme slide; consumers (e.g. a
/// `LoginSlideView` with `LoginSlideSource::PrivacySettingsFromTerminalIntentionTheme`)
/// rely on that to select the right visual / back-routing behavior.
PrivacySettingsFromTerminalThemeSlideRequested,
UpgradeRequested,
UpgradeCopyUrlRequested,
UpgradePasteTokenFromClipboardRequested,
/// Emitted when the app regains focus (e.g. user returns from the browser).
/// The parent should refresh any stale data: available models, workspace/billing metadata, etc.
AppBecameActive,
}
pub struct AgentOnboardingView {
onboarding_state: ModelHandle<OnboardingStateModel>,
intro_slide: ViewHandle<IntroSlide>,
theme_picker_slide: ViewHandle<ThemePickerSlide>,
intention_slide: ViewHandle<IntentionSlide>,
ai_setup_slide: ViewHandle<AiSetupSlide>,
customize_slide: ViewHandle<CustomizeUISlide>,
agent_slide: ViewHandle<AgentSlide>,
ai_access_slide: ViewHandle<AiAccessSlide>,
third_party_slide: ViewHandle<ThirdPartySlide>,
project_slide: ViewHandle<ProjectSlide>,
skippable: bool,
close_button: button::Button,
no_ai_confirm_button: button::Button,
no_ai_cancel_button: button::Button,
no_ai_close_button: button::Button,
last_model_refresh: Option<Instant>,
show_plan_activated_toast: bool,
last_auth_state: OnboardingAuthState,
plan_activated_close_mouse_state: MouseStateHandle,
}
#[derive(Clone, Copy, Debug)]
pub enum AgentOnboardingAction {
UpKey,
DownKey,
LeftKey,
RightKey,
TabKey,
EnterKey,
CmdOrCtrlEnterKey,
Escape,
NoAiConfirm,
NoAiCancel,
NoAiDismiss,
DismissPlanActivatedToast,
}
fn dispatch_onboarding_action_to_slide<V: OnboardingSlide>(
slide: &mut V,
action: AgentOnboardingAction,
ctx: &mut ViewContext<V>,
) {
match action {
AgentOnboardingAction::UpKey => slide.on_up(ctx),
AgentOnboardingAction::DownKey => slide.on_down(ctx),
AgentOnboardingAction::LeftKey => slide.on_left(ctx),
AgentOnboardingAction::RightKey => slide.on_right(ctx),
AgentOnboardingAction::TabKey => slide.on_tab(ctx),
AgentOnboardingAction::EnterKey => slide.on_enter(ctx),
AgentOnboardingAction::CmdOrCtrlEnterKey => slide.on_cmd_or_ctrl_enter(ctx),
AgentOnboardingAction::Escape => slide.on_escape(ctx),
// Parent-level actions are handled by the parent view, never routed to a slide.
AgentOnboardingAction::NoAiConfirm
| AgentOnboardingAction::NoAiCancel
| AgentOnboardingAction::NoAiDismiss
| AgentOnboardingAction::DismissPlanActivatedToast => {}
}
}
impl AgentOnboardingView {
/// Creates a new AgentOnboardingView.
#[allow(clippy::too_many_arguments)]
pub fn new(
theme_picker_themes: [GalaxyTheme; 4],
skippable: bool,
models: Vec<OnboardingModelInfo>,
default_model_id: LLMId,
workspace_enforces_autonomy: bool,
agent_modality_enabled: bool,
auth_state: OnboardingAuthState,
ctx: &mut ViewContext<Self>,
) -> Self {
let onboarding_state = ctx.add_model(|_| {
OnboardingStateModel::new(
models,
default_model_id,
workspace_enforces_autonomy,
agent_modality_enabled,
auth_state,
)
});
ctx.subscribe_to_model(&onboarding_state, |me, _model, event, ctx| {
// Re-render when slide selection changes.
if !ctx.is_self_or_child_focused() {
ctx.focus_self();
}
ctx.notify();
match event {
OnboardingStateEvent::Completed => {
me.handle_onboarding_completed(ctx);
}
OnboardingStateEvent::UpgradeRequested => {
ctx.emit(AgentOnboardingEvent::UpgradeRequested);
}
OnboardingStateEvent::AuthStateChanged => {
me.handle_auth_state_changed(ctx);
}
OnboardingStateEvent::ModelsUpdated
| OnboardingStateEvent::SelectedSlideChanged
| OnboardingStateEvent::IntentionChanged
| OnboardingStateEvent::NoAiConfirmationChanged => {}
}
});
let intro_slide = {
let onboarding_state = onboarding_state.clone();
ctx.add_typed_action_view(move |_| IntroSlide::new(onboarding_state))
};
ctx.subscribe_to_view(&intro_slide, |_me, _view, event, ctx| match event {
IntroSlideEvent::LoginRequested => {
ctx.emit(AgentOnboardingEvent::LoginFromWelcomeRequested);
}
});
let theme_picker_slide = {
let themes = theme_picker_themes.clone();
let onboarding_state = onboarding_state.clone();
ctx.add_typed_action_view(move |ctx| {
ThemePickerSlide::new(themes.clone(), onboarding_state, ctx)
})
};
let intention_slide = {
let onboarding_state = onboarding_state.clone();
ctx.add_typed_action_view(move |_| IntentionSlide::new(onboarding_state))
};
let ai_setup_slide = {
let onboarding_state = onboarding_state.clone();
ctx.add_typed_action_view(move |_| AiSetupSlide::new(onboarding_state))
};
let customize_slide = {
let onboarding_state = onboarding_state.clone();
ctx.add_typed_action_view(move |ctx| CustomizeUISlide::new(onboarding_state, ctx))
};
ctx.subscribe_to_view(&theme_picker_slide, |me, _view, event, ctx| {
me.handle_theme_picker_slide_event(event, ctx);
});
let agent_slide = {
let onboarding_state = onboarding_state.clone();
ctx.add_typed_action_view(move |ctx| AgentSlide::new(onboarding_state, ctx))
};
let ai_access_slide = {
let onboarding_state = onboarding_state.clone();
ctx.add_typed_action_view(move |_| AiAccessSlide::new(onboarding_state))
};
ctx.subscribe_to_view(&ai_access_slide, |_me, _view, event, ctx| match event {
AiAccessSlideEvent::CopyUpgradeUrlRequested => {
ctx.emit(AgentOnboardingEvent::UpgradeCopyUrlRequested);
}
AiAccessSlideEvent::PasteAuthTokenFromClipboardRequested => {
ctx.emit(AgentOnboardingEvent::UpgradePasteTokenFromClipboardRequested);
}
});
let third_party_slide = {
let onboarding_state = onboarding_state.clone();
ctx.add_typed_action_view(move |ctx| ThirdPartySlide::new(onboarding_state, ctx))
};
let project_slide = {
let onboarding_state = onboarding_state.clone();
ctx.add_typed_action_view(move |_| ProjectSlide::new(onboarding_state))
};
// When the app regains focus (e.g. user returning from the upgrade page in the
// browser), notify the parent to refresh models and workspace/billing metadata.
// Debounced to avoid excessive API calls from rapid alt-tabbing.
ctx.subscribe_to_model(&WindowManager::handle(ctx), |me, _wm, event, ctx| {
let StateEvent::ValueChanged { current, previous } = event;
if previous.stage != ApplicationStage::Active
&& current.stage == ApplicationStage::Active
{
let now = Instant::now();
let should_refresh = me
.last_model_refresh
.is_none_or(|last| now.duration_since(last) >= APP_BECAME_ACTIVE_DEBOUNCE);
if should_refresh {
me.last_model_refresh = Some(now);
ctx.emit(AgentOnboardingEvent::AppBecameActive);
}
}
});
Self {
onboarding_state,
intro_slide,
theme_picker_slide,
intention_slide,
ai_setup_slide,
customize_slide,
agent_slide,
ai_access_slide,
third_party_slide,
project_slide,
skippable,
close_button: button::Button::default(),
no_ai_confirm_button: button::Button::default(),
no_ai_cancel_button: button::Button::default(),
no_ai_close_button: button::Button::default(),
last_model_refresh: None,
show_plan_activated_toast: false,
last_auth_state: auth_state,
plan_activated_close_mouse_state: MouseStateHandle::default(),
}
}
/// Updates the list of available models.
pub fn set_onboarding_models(
&mut self,
models: Vec<OnboardingModelInfo>,
default_model_id: LLMId,
ctx: &mut ViewContext<Self>,
) {
self.onboarding_state.update(ctx, |state, ctx| {
state.set_models(models, default_model_id, ctx);
});
ctx.notify();
}
pub fn set_workspace_enforces_autonomy(&mut self, value: bool, ctx: &mut ViewContext<Self>) {
self.onboarding_state.update(ctx, |state, ctx| {
state.set_workspace_enforces_autonomy(value, ctx);
});
ctx.notify();
}
pub fn set_auth_state(&mut self, auth_state: OnboardingAuthState, ctx: &mut ViewContext<Self>) {
self.onboarding_state.update(ctx, |state, ctx| {
state.set_auth_state(auth_state, ctx);
});
ctx.notify();
}
/// The current `use_vertical_tabs` value on the onboarding UI customization.
/// This reflects the intention's default (agent = vertical, terminal = horizontal)
/// and any change the user made on the customize slide, and is what the
/// theme slide uses to pick its right-panel image.
pub fn use_vertical_tabs(&self, ctx: &AppContext) -> bool {
self.onboarding_state
.as_ref(ctx)
.ui_customization()
.use_vertical_tabs
}
pub fn start_onboarding(&self, ctx: &mut ViewContext<Self>) {
// Focus the onboarding view so key bindings (Enter, arrow keys, etc.) are routed here
// instead of to other views (e.g. the editor).
ctx.focus_self();
// Preload customize-slide images so they're ready when the user reaches that slide.
if FeatureFlag::OpenWarpNewSettingsModes.is_enabled() {
Self::preload_onboarding_images(ctx);
}
send_telemetry_from_ctx!(OnboardingEvent::OnboardingStarted, ctx);
send_telemetry_from_ctx!(
OnboardingEvent::SlideViewed {
slide_name: "intro".to_string(),
},
ctx
);
}
/// Eagerly loads all onboarding slide images into the asset cache
/// so they display instantly when the user navigates between slides.
fn preload_onboarding_images(ctx: &mut ViewContext<Self>) {
let asset_cache = galaxyui_core::assets::asset_cache::AssetCache::as_ref(ctx);
// Preload the shared background image used on all right panels.
asset_cache.load_asset::<ImageType>(AssetSource::Bundled {
path: crate::slides::layout::ONBOARDING_BG_PATH,
});
for path in IntentionSlide::VISUAL_IMAGE_PATHS {
asset_cache.load_asset::<ImageType>(AssetSource::Bundled { path });
}
for path in AiSetupSlide::VISUAL_IMAGE_PATHS {
asset_cache.load_asset::<ImageType>(AssetSource::Bundled { path });
}
for path in AiAccessSlide::VISUAL_IMAGE_PATHS {
asset_cache.load_asset::<ImageType>(AssetSource::Bundled { path });
}
for path in CustomizeUISlide::VISUAL_IMAGE_PATHS {
asset_cache.load_asset::<ImageType>(AssetSource::Bundled { path });
}
for path in ThirdPartySlide::VISUAL_IMAGE_PATHS {
asset_cache.load_asset::<ImageType>(AssetSource::Bundled { path });
}
for path in ThemePickerSlide::VISUAL_IMAGE_PATHS {
asset_cache.load_asset::<ImageType>(AssetSource::Bundled { path });
}
// Agent slide reuses customize_vertical_tabs / customize_horizontal_tabs
// which are already in CustomizeUISlide::VISUAL_IMAGE_PATHS.
}
fn render_no_ai_dialog(&self, appearance: &Appearance) -> Box<dyn Element> {
let escape = Keystroke::parse("escape").unwrap_or_default();
let close_button = self.no_ai_close_button.render(
appearance,
button::Params {
content: button::Content::Icon(Icon::X),
theme: &button::themes::Naked,
options: button::Options {
keystroke: Some(escape),
on_click: Some(Box::new(|ctx, _app, _pos| {
ctx.dispatch_typed_action(AgentOnboardingAction::NoAiDismiss);
})),
..button::Options::default(appearance)
},
},
);
let cancel_button = self.no_ai_cancel_button.render(
appearance,
button::Params {
content: button::Content::Label("Give me AI features".into()),
theme: &button::themes::Naked,
options: button::Options {
on_click: Some(Box::new(|ctx, _app, _pos| {
ctx.dispatch_typed_action(AgentOnboardingAction::NoAiCancel);
})),
..button::Options::default(appearance)
},
},
);
let enter = Keystroke::parse("enter").unwrap_or_default();
let confirm_button = self.no_ai_confirm_button.render(
appearance,
button::Params {
content: button::Content::Label("I don't want AI".into()),
theme: &button::themes::Primary,
options: button::Options {
keystroke: Some(enter),
on_click: Some(Box::new(|ctx, _app, _pos| {
ctx.dispatch_typed_action(AgentOnboardingAction::NoAiConfirm);
})),
..button::Options::default(appearance)
},
},
);
render_feature_optout_dialog(
appearance,
FeatureOptOutDialog {
title: "Are you sure you don't want AI?",
body: "Without AI, you'll still get Warp's terminal experience, but you'll miss \
our agentic features like automatic fixes for terminal errors.",
features: &[],
close_button,
cancel_button,
confirm_button,
},
)
}
fn handle_onboarding_completed(&mut self, ctx: &mut ViewContext<Self>) {
let settings = self.onboarding_state.as_ref(ctx).settings();
ctx.emit(AgentOnboardingEvent::OnboardingCompleted(settings));
}
/// Reacts to a billing/auth transition. When the user becomes a paying user
/// we show a success toast; if they're still on the AI-access slide we also
/// advance them, since selecting a plan was the remaining action there.
fn handle_auth_state_changed(&mut self, ctx: &mut ViewContext<Self>) {
let new_state = self.onboarding_state.as_ref(ctx).auth_state();
let became_paying = new_state == OnboardingAuthState::PayingUser
&& self.last_auth_state != OnboardingAuthState::PayingUser;
self.last_auth_state = new_state;
if !became_paying {
return;
}
let on_ai_access = self.onboarding_state.as_ref(ctx).step() == OnboardingStep::AiAccess;
if on_ai_access {
self.onboarding_state
.update(ctx, |model, ctx| model.next(ctx));
}
self.show_plan_activated_toast = true;
let _ = ctx.spawn(
galaxyui_core::r#async::Timer::after(PLAN_ACTIVATED_TOAST_DURATION),
|me: &mut Self, _, ctx| {
if me.show_plan_activated_toast {
me.show_plan_activated_toast = false;
ctx.notify();
}
},
);
}
/// Green success pill shown after billing succeeds. Hosted at the view level
/// (not the slide) so it survives the auto-advance off the AI-access slide.
fn render_plan_activated_toast(&self, appearance: &Appearance) -> Box<dyn Element> {
const TOAST_MIN_HEIGHT: f32 = 40.;
const ICON_SIZE: f32 = 14.;
const CLOSE_SIZE: f32 = 16.;
const FONT_SIZE: f32 = 12.;
let theme = appearance.theme();
let toast_bg: Fill = theme.ansi_fg_green().into();
let text_color: ColorU = theme.font_color(toast_bg.into_solid()).into();
let ui_builder = appearance.ui_builder();
let check_icon = ConstrainedBox::new(Box::new(
Icon::CheckSkinny.to_galaxyui_icon(Fill::Solid(text_color)),
))
.with_width(ICON_SIZE)
.with_height(ICON_SIZE)
.finish();
let text = ui_builder
.span("Plan successfully activated!")
.with_style(UiComponentStyles {
font_color: Some(text_color),
font_size: Some(FONT_SIZE),
font_weight: Some(Weight::Medium),
..Default::default()
})
.build()
.finish();
let close_button = ui_builder
.close_button(CLOSE_SIZE, self.plan_activated_close_mouse_state.clone())
.with_style(UiComponentStyles {
font_color: Some(text_color),
..Default::default()
})
.build()
.on_click(|ctx, _, _| {
ctx.dispatch_typed_action(AgentOnboardingAction::DismissPlanActivatedToast);
})
.finish();
let left = Flex::row()
.with_cross_axis_alignment(CrossAxisAlignment::Center)
.with_child(check_icon)
.with_child(Container::new(text).with_margin_left(8.).finish())
.finish();
let row = Flex::row()
.with_main_axis_size(MainAxisSize::Max)
.with_main_axis_alignment(MainAxisAlignment::SpaceBetween)
.with_cross_axis_alignment(CrossAxisAlignment::Center)
.with_child(left)
.with_child(close_button)
.finish();
ConstrainedBox::new(
Container::new(row)
.with_background(toast_bg)
.with_horizontal_padding(16.)
.finish(),
)
.with_min_height(TOAST_MIN_HEIGHT)
.finish()
}
fn handle_theme_picker_slide_event(
&mut self,
event: &ThemePickerSlideEvent,
ctx: &mut ViewContext<Self>,
) {
match event {
ThemePickerSlideEvent::ThemeSelected { theme_name } => {
ctx.emit(AgentOnboardingEvent::ThemeSelected {
theme_name: theme_name.clone(),
});
}
ThemePickerSlideEvent::SyncWithOsToggled { enabled } => {
ctx.emit(AgentOnboardingEvent::SyncWithOsToggled { enabled: *enabled });
}
ThemePickerSlideEvent::PrivacySettingsRequested => {
ctx.emit(AgentOnboardingEvent::PrivacySettingsFromTerminalThemeSlideRequested);
}
}
}
}
impl Entity for AgentOnboardingView {
type Event = AgentOnboardingEvent;
}
impl View for AgentOnboardingView {
fn ui_name() -> &'static str {
"AgentOnboardingView"
}
fn render(&self, app: &AppContext) -> Box<dyn Element> {
let appearance = Appearance::as_ref(app);
let theme = appearance.theme();
let mut stack = Stack::new();
if let Some(img) = theme.background_image() {
// Render the image behind everything.
stack.add_child(
Shrinkable::new(
1.,
Image::new(img.source(), CacheOption::Original)
.cover()
.finish(),
)
.finish(),
);
// Overlay the theme background so the image shows through at img.opacity.
let overlay_opacity = (100u8).saturating_sub(img.opacity);
stack.add_child(
Rect::new()
.with_background(theme.background().with_opacity(overlay_opacity))
.finish(),
);
} else {
stack.add_child(
Container::new(Empty::new().finish())
.with_background(theme.background())
.finish(),
);
}
let selected_slide = self.onboarding_state.as_ref(app).step();
let slide = match selected_slide {
OnboardingStep::Intro => ChildView::new(&self.intro_slide).finish(),
OnboardingStep::ThemePicker => ChildView::new(&self.theme_picker_slide).finish(),
OnboardingStep::Intention => ChildView::new(&self.intention_slide).finish(),
OnboardingStep::AiSetup => ChildView::new(&self.ai_setup_slide).finish(),
OnboardingStep::Customize => ChildView::new(&self.customize_slide).finish(),
OnboardingStep::Agent => ChildView::new(&self.agent_slide).finish(),
OnboardingStep::AiAccess => ChildView::new(&self.ai_access_slide).finish(),
OnboardingStep::ThirdParty => ChildView::new(&self.third_party_slide).finish(),
OnboardingStep::Project => ChildView::new(&self.project_slide).finish(),
};
stack.add_child(slide);
if self.skippable {
let esc = Keystroke::parse("escape").unwrap_or_default();
let close_button = self.close_button.render(
appearance,
button::Params {
content: button::Content::Label("Skip".into()),
theme: &button::themes::Naked,
options: button::Options {
size: button::Size::Small,
keystroke: Some(esc),
on_click: Some(Box::new(|ctx, _app, _pos| {
ctx.dispatch_typed_action(AgentOnboardingAction::Escape);
})),
..button::Options::default(appearance)
},
},
);
stack.add_positioned_child(
close_button,
OffsetPositioning::offset_from_parent(
vec2f(-24., 24.),
ParentOffsetBounds::WindowByPosition,
ParentAnchor::TopRight,
ChildAnchor::TopRight,
),
);
}
if self
.onboarding_state
.as_ref(app)
.no_ai_confirmation()
.is_some()
{
let dialog = self.render_no_ai_dialog(appearance);
stack.add_child(
Rect::new()
.with_background(Fill::Solid(ColorU::black()).with_opacity(60))
.finish(),
);
stack.add_child(
Dismiss::new(Align::new(dialog).finish())
.on_dismiss(|ctx, _app| {
ctx.dispatch_typed_action(AgentOnboardingAction::NoAiDismiss);
})
.finish(),
);
}
if self.show_plan_activated_toast {
stack.add_child(
Align::new(self.render_plan_activated_toast(appearance))
.bottom_center()
.finish(),
);
}
stack.finish()
}
}
impl TypedActionView for AgentOnboardingView {
type Action = AgentOnboardingAction;
fn handle_action(&mut self, action: &Self::Action, ctx: &mut ViewContext<Self>) {
if self
.onboarding_state
.as_ref(ctx)
.no_ai_confirmation()
.is_some()
{
match action {
AgentOnboardingAction::NoAiConfirm | AgentOnboardingAction::EnterKey => {
self.onboarding_state
.update(ctx, |model, ctx| model.confirm_no_ai(ctx));
}
AgentOnboardingAction::NoAiCancel => {
self.onboarding_state
.update(ctx, |model, ctx| model.cancel_no_ai(ctx));
}
AgentOnboardingAction::NoAiDismiss | AgentOnboardingAction::Escape => {
self.onboarding_state
.update(ctx, |model, ctx| model.dismiss_no_ai(ctx));
}
_ => {}
}
return;
}
if matches!(action, AgentOnboardingAction::Escape) && self.skippable {
ctx.emit(AgentOnboardingEvent::OnboardingSkipped);
return;
}
if matches!(action, AgentOnboardingAction::DismissPlanActivatedToast) {
self.show_plan_activated_toast = false;
ctx.notify();
return;
}
let selected_slide = self.onboarding_state.as_ref(ctx).step();
match selected_slide {
OnboardingStep::Intro => self.intro_slide.update(ctx, |slide, ctx| {
dispatch_onboarding_action_to_slide(slide, *action, ctx)
}),
OnboardingStep::ThemePicker => self.theme_picker_slide.update(ctx, |slide, ctx| {
dispatch_onboarding_action_to_slide(slide, *action, ctx)
}),
OnboardingStep::Intention => self.intention_slide.update(ctx, |slide, ctx| {
dispatch_onboarding_action_to_slide(slide, *action, ctx)
}),
OnboardingStep::AiSetup => self.ai_setup_slide.update(ctx, |slide, ctx| {
dispatch_onboarding_action_to_slide(slide, *action, ctx)
}),
OnboardingStep::Customize => self.customize_slide.update(ctx, |slide, ctx| {
dispatch_onboarding_action_to_slide(slide, *action, ctx)
}),
OnboardingStep::Agent => self.agent_slide.update(ctx, |slide, ctx| {
dispatch_onboarding_action_to_slide(slide, *action, ctx)
}),
OnboardingStep::AiAccess => self.ai_access_slide.update(ctx, |slide, ctx| {
dispatch_onboarding_action_to_slide(slide, *action, ctx)
}),
OnboardingStep::ThirdParty => self.third_party_slide.update(ctx, |slide, ctx| {
dispatch_onboarding_action_to_slide(slide, *action, ctx)
}),
OnboardingStep::Project => self.project_slide.update(ctx, |slide, ctx| {
dispatch_onboarding_action_to_slide(slide, *action, ctx)
}),
}
}
}
pub fn init(app: &mut AppContext) {
app.register_fixed_bindings([
FixedBinding::new(
"up",
AgentOnboardingAction::UpKey,
id!(AgentOnboardingView::ui_name()),
),
FixedBinding::new(
"down",
AgentOnboardingAction::DownKey,
id!(AgentOnboardingView::ui_name()),
),
FixedBinding::new(
"left",
AgentOnboardingAction::LeftKey,
id!(AgentOnboardingView::ui_name()),
),
FixedBinding::new(
"right",
AgentOnboardingAction::RightKey,
id!(AgentOnboardingView::ui_name()),
),
FixedBinding::new(
"tab",
AgentOnboardingAction::TabKey,
id!(AgentOnboardingView::ui_name()),
),
FixedBinding::new(
"enter",
AgentOnboardingAction::EnterKey,
id!(AgentOnboardingView::ui_name()),
),
FixedBinding::new(
"numpadenter",
AgentOnboardingAction::EnterKey,
id!(AgentOnboardingView::ui_name()),
),
FixedBinding::new(
"cmdorctrl-enter",
AgentOnboardingAction::CmdOrCtrlEnterKey,
id!(AgentOnboardingView::ui_name()),
),
FixedBinding::new(
"cmdorctrl-numpadenter",
AgentOnboardingAction::CmdOrCtrlEnterKey,
id!(AgentOnboardingView::ui_name()),
),
FixedBinding::new(
"escape",
AgentOnboardingAction::Escape,
id!(AgentOnboardingView::ui_name()),
),
]);
}