first pass of merging in warp (doesn't build)
This commit is contained in:
@@ -1,74 +1,33 @@
|
||||
use super::two_line_button::{render_two_line_button, TwoLineButtonSpec};
|
||||
use crate::model::{OnboardingAuthState, OnboardingStateEvent, OnboardingStateModel};
|
||||
use crate::slides::{bottom_nav, layout, slide_content};
|
||||
use crate::telemetry::OnboardingEvent;
|
||||
use galaxy_core::send_telemetry_from_ctx;
|
||||
|
||||
use super::OnboardingSlide;
|
||||
use crate::visuals::agent_visual;
|
||||
use galaxy_core::features::FeatureFlag;
|
||||
use galaxy_core::ui::{
|
||||
appearance::Appearance,
|
||||
theme::{color::internal_colors, Fill},
|
||||
};
|
||||
use galaxyui::{
|
||||
elements::{
|
||||
AnchorPair, Border, ClippedScrollStateHandle, ClippedScrollable, ConstrainedBox, Container,
|
||||
CornerRadius, CrossAxisAlignment, Dismiss, Empty, Flex, FormattedTextElement, Hoverable,
|
||||
Icon as WarpUiIcon, MainAxisAlignment, MainAxisSize, MouseStateHandle, OffsetPositioning,
|
||||
OffsetType, ParentElement, ParentOffsetBounds, PositioningAxis, Radius, SavePosition,
|
||||
ScrollTarget, ScrollToPositionMode, ScrollbarWidth, Stack, Text, XAxisAnchor, YAxisAnchor,
|
||||
},
|
||||
fonts::Properties,
|
||||
fonts::Weight,
|
||||
keymap::Keystroke,
|
||||
platform::Cursor,
|
||||
scene::DropShadow,
|
||||
text_layout::TextAlignment,
|
||||
ui_components::components::{UiComponent as _, UiComponentStyles},
|
||||
AppContext, Element, Entity, Gradient, SingletonEntity as _, TypedActionView, View,
|
||||
ViewContext,
|
||||
};
|
||||
use pathfinder_geometry::vector::vec2f;
|
||||
use ui_components::{button, Component as _, Options as _};
|
||||
|
||||
use ai::LLMId;
|
||||
use galaxy_core::ui::icons::Icon;
|
||||
use pathfinder_color::ColorU;
|
||||
use ui_components::button::State as ButtonState;
|
||||
use ui_components::{button, Component as _, Options as _};
|
||||
use galaxy_core::features::FeatureFlag;
|
||||
use galaxy_core::ui::appearance::Appearance;
|
||||
use galaxy_core::ui::theme::color::internal_colors;
|
||||
use galaxy_core::ui::theme::Fill;
|
||||
use galaxyui_core::elements::{
|
||||
AnchorPair, Border, ClippedScrollStateHandle, ClippedScrollable, ConstrainedBox, Container,
|
||||
CornerRadius, CrossAxisAlignment, Dismiss, Empty, Flex, FormattedTextElement, Hoverable,
|
||||
Icon as WarpUiIcon, MainAxisAlignment, MainAxisSize, MouseStateHandle, OffsetPositioning,
|
||||
OffsetType, ParentElement, ParentOffsetBounds, PositioningAxis, Radius, SavePosition,
|
||||
ScrollTarget, ScrollToPositionMode, ScrollbarWidth, Stack, Text, XAxisAnchor, YAxisAnchor,
|
||||
};
|
||||
use galaxyui_core::fonts::{Properties, Weight};
|
||||
use galaxyui_core::keymap::Keystroke;
|
||||
use galaxyui_core::platform::Cursor;
|
||||
use galaxyui_core::scene::DropShadow;
|
||||
use galaxyui_core::text_layout::TextAlignment;
|
||||
use galaxyui_core::ui_components::components::{UiComponent as _, UiComponentStyles};
|
||||
use galaxyui_core::{
|
||||
AppContext, Element, Entity, SingletonEntity as _, TypedActionView, View, ViewContext,
|
||||
};
|
||||
|
||||
/// high-contrast "inverted" fill (foreground color)
|
||||
struct UpgradeButtonTheme;
|
||||
|
||||
impl button::Theme for UpgradeButtonTheme {
|
||||
fn background(
|
||||
&self,
|
||||
button_state: ButtonState,
|
||||
appearance: &Appearance,
|
||||
) -> Option<galaxy_core::ui::theme::Fill> {
|
||||
use galaxy_core::ui::color::blend::Blend;
|
||||
let theme = appearance.theme();
|
||||
let base = theme.foreground();
|
||||
match button_state {
|
||||
ButtonState::Default => Some(base),
|
||||
// Blend a little of the theme background back in to dim on hover /
|
||||
// press. Opacities are relative to the foreground fill.
|
||||
ButtonState::Hovered => Some(base.blend(&theme.background().with_opacity(15))),
|
||||
ButtonState::Pressed => Some(base.blend(&theme.background().with_opacity(30))),
|
||||
}
|
||||
}
|
||||
|
||||
fn text_color(
|
||||
&self,
|
||||
background: Option<galaxy_core::ui::theme::Fill>,
|
||||
appearance: &Appearance,
|
||||
) -> ColorU {
|
||||
let bg = background
|
||||
.unwrap_or_else(|| appearance.theme().background())
|
||||
.into_solid();
|
||||
appearance.theme().font_color(bg).into()
|
||||
}
|
||||
}
|
||||
use super::two_line_button::{render_two_line_button, TwoLineButtonSpec};
|
||||
use super::OnboardingSlide;
|
||||
use crate::model::{OnboardingStateEvent, OnboardingStateModel};
|
||||
use crate::slides::{bottom_nav, layout, slide_content};
|
||||
use crate::visuals::agent_visual;
|
||||
|
||||
/// Information about a model displayed on the onboarding slide.
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -76,7 +35,6 @@ pub struct OnboardingModelInfo {
|
||||
pub id: LLMId,
|
||||
pub title: String,
|
||||
pub icon: Icon,
|
||||
pub requires_upgrade: bool,
|
||||
pub is_default: bool,
|
||||
}
|
||||
|
||||
@@ -139,20 +97,10 @@ pub enum AgentSlideAction {
|
||||
ToggleDisableOz,
|
||||
BackClicked,
|
||||
NextClicked,
|
||||
UpgradeClicked,
|
||||
CopyUpgradeUrlClicked,
|
||||
PasteAuthTokenFromClipboardClicked,
|
||||
DismissPlanActivatedToast,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum AgentSlideEvent {
|
||||
CopyUpgradeUrlRequested,
|
||||
PasteAuthTokenFromClipboardRequested,
|
||||
}
|
||||
|
||||
pub struct AgentSlide {
|
||||
onboarding_state: galaxyui::ModelHandle<OnboardingStateModel>,
|
||||
onboarding_state: galaxyui_core::ModelHandle<OnboardingStateModel>,
|
||||
|
||||
/// Mouse state handles for each model row.
|
||||
model_mouse_states: Vec<MouseStateHandle>,
|
||||
@@ -164,25 +112,14 @@ pub struct AgentSlide {
|
||||
autonomy_partial_mouse_state: MouseStateHandle,
|
||||
autonomy_none_mouse_state: MouseStateHandle,
|
||||
|
||||
disable_oz_mouse: MouseStateHandle,
|
||||
|
||||
back_button: button::Button,
|
||||
next_button: button::Button,
|
||||
upgrade_button: button::Button,
|
||||
scroll_state: ClippedScrollStateHandle,
|
||||
dropdown_scroll_state: ClippedScrollStateHandle,
|
||||
is_model_list_expanded: bool,
|
||||
highlighted_model_id: Option<LLMId>,
|
||||
show_auth_prompt_bar: bool,
|
||||
copy_url_mouse_state: MouseStateHandle,
|
||||
paste_token_mouse_state: MouseStateHandle,
|
||||
show_plan_activated_toast: bool,
|
||||
last_auth_state: OnboardingAuthState,
|
||||
plan_activated_close_mouse_state: MouseStateHandle,
|
||||
}
|
||||
|
||||
const PLAN_ACTIVATED_TOAST_DURATION: std::time::Duration = std::time::Duration::from_secs(5);
|
||||
|
||||
/// Produces the `SavePosition` id for the model row at `index` in the
|
||||
/// dropdown list. Used by `scroll_to_position` to scroll a specific row into
|
||||
/// view as the keyboard highlight moves.
|
||||
@@ -190,18 +127,9 @@ fn model_row_position_id(index: usize) -> String {
|
||||
format!("agent_slide_model_row_{index}")
|
||||
}
|
||||
|
||||
/// Returns the slide's view of the model list: free-tier before premium,
|
||||
/// with server order preserved within each tier. The slide owns this sort so
|
||||
/// state storage can stay in server order.
|
||||
fn sorted_models(models: &[OnboardingModelInfo]) -> Vec<OnboardingModelInfo> {
|
||||
let (free, premium): (Vec<_>, Vec<_>) =
|
||||
models.iter().cloned().partition(|m| !m.requires_upgrade);
|
||||
free.into_iter().chain(premium).collect()
|
||||
}
|
||||
|
||||
impl AgentSlide {
|
||||
pub(crate) fn new(
|
||||
onboarding_state: galaxyui::ModelHandle<OnboardingStateModel>,
|
||||
onboarding_state: galaxyui_core::ModelHandle<OnboardingStateModel>,
|
||||
ctx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let model_count = onboarding_state.as_ref(ctx).models().len();
|
||||
@@ -209,8 +137,6 @@ impl AgentSlide {
|
||||
.map(|_| MouseStateHandle::default())
|
||||
.collect();
|
||||
|
||||
let initial_auth_state = onboarding_state.as_ref(ctx).auth_state();
|
||||
|
||||
ctx.subscribe_to_model(&onboarding_state, |me, model, event, ctx| {
|
||||
match event {
|
||||
OnboardingStateEvent::ModelsUpdated => {
|
||||
@@ -226,30 +152,12 @@ impl AgentSlide {
|
||||
let model_count = state.models().len();
|
||||
me.ensure_mouse_states_for_models(model_count, ctx);
|
||||
}
|
||||
OnboardingStateEvent::AuthStateChanged => {
|
||||
let new_state = model.as_ref(ctx).auth_state();
|
||||
if new_state == OnboardingAuthState::PayingUser
|
||||
&& me.last_auth_state != OnboardingAuthState::PayingUser
|
||||
{
|
||||
me.show_plan_activated_toast = true;
|
||||
// Auto-dismiss after the configured duration.
|
||||
let _ = ctx.spawn(
|
||||
galaxyui::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();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
me.last_auth_state = new_state;
|
||||
ctx.notify();
|
||||
}
|
||||
OnboardingStateEvent::SelectedSlideChanged
|
||||
OnboardingStateEvent::AuthStateChanged
|
||||
| OnboardingStateEvent::SelectedSlideChanged
|
||||
| OnboardingStateEvent::IntentionChanged
|
||||
| OnboardingStateEvent::Completed
|
||||
| OnboardingStateEvent::UpgradeRequested => {}
|
||||
| OnboardingStateEvent::UpgradeRequested
|
||||
| OnboardingStateEvent::NoAiConfirmationChanged => {}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -260,20 +168,12 @@ impl AgentSlide {
|
||||
autonomy_full_mouse_state: MouseStateHandle::default(),
|
||||
autonomy_partial_mouse_state: MouseStateHandle::default(),
|
||||
autonomy_none_mouse_state: MouseStateHandle::default(),
|
||||
disable_oz_mouse: MouseStateHandle::default(),
|
||||
back_button: button::Button::default(),
|
||||
next_button: button::Button::default(),
|
||||
upgrade_button: button::Button::default(),
|
||||
scroll_state: ClippedScrollStateHandle::new(),
|
||||
dropdown_scroll_state: ClippedScrollStateHandle::new(),
|
||||
is_model_list_expanded: false,
|
||||
highlighted_model_id: None,
|
||||
show_auth_prompt_bar: false,
|
||||
copy_url_mouse_state: MouseStateHandle::default(),
|
||||
paste_token_mouse_state: MouseStateHandle::default(),
|
||||
show_plan_activated_toast: false,
|
||||
last_auth_state: initial_auth_state,
|
||||
plan_activated_close_mouse_state: MouseStateHandle::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -308,7 +208,7 @@ impl AgentSlide {
|
||||
// state is a floating overlay (built in `View::render`) that sits *on top
|
||||
// of* this content, so the underlying layout never shifts between the two
|
||||
// states. That keeps the header + picker chip pinned in place.
|
||||
let bottom_nav = self.render_bottom_nav(appearance);
|
||||
let bottom_nav = self.render_bottom_nav(appearance, app);
|
||||
slide_content::onboarding_slide_content(
|
||||
vec![
|
||||
self.render_header(appearance),
|
||||
@@ -333,7 +233,7 @@ impl AgentSlide {
|
||||
.finish();
|
||||
|
||||
let subtitle = FormattedTextElement::from_str(
|
||||
"Select your in-app agent's defaults.",
|
||||
"Select your Warp Agent's defaults.",
|
||||
appearance.ui_font_family(),
|
||||
16.,
|
||||
)
|
||||
@@ -389,19 +289,10 @@ impl AgentSlide {
|
||||
upper_col.finish()
|
||||
};
|
||||
|
||||
let mut col = Flex::column()
|
||||
let col = Flex::column()
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Start)
|
||||
.with_child(upper_sections);
|
||||
|
||||
if FeatureFlag::OpenWarpNewSettingsModes.is_enabled() {
|
||||
let disable_oz_section = self.render_disable_oz_section(appearance, settings);
|
||||
col = col.with_child(
|
||||
Container::new(disable_oz_section)
|
||||
.with_margin_top(24.)
|
||||
.finish(),
|
||||
);
|
||||
}
|
||||
|
||||
Container::new(col.finish()).with_margin_top(40.).finish()
|
||||
}
|
||||
|
||||
@@ -458,31 +349,15 @@ impl AgentSlide {
|
||||
);
|
||||
}
|
||||
|
||||
let has_disabled = self
|
||||
.onboarding_state
|
||||
.as_ref(app)
|
||||
.models()
|
||||
.iter()
|
||||
.any(|m| m.requires_upgrade);
|
||||
|
||||
let mut col = Flex::column()
|
||||
Flex::column()
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Stretch)
|
||||
.with_child(header)
|
||||
.with_child(
|
||||
Container::new(chip_stack.finish())
|
||||
.with_margin_top(12.)
|
||||
.finish(),
|
||||
);
|
||||
|
||||
if has_disabled {
|
||||
col = col.with_child(
|
||||
Container::new(self.render_upgrade_banner(appearance))
|
||||
.with_margin_top(12.)
|
||||
.finish(),
|
||||
);
|
||||
}
|
||||
|
||||
col.finish()
|
||||
)
|
||||
.finish()
|
||||
}
|
||||
|
||||
/// Renders the single-row collapsed picker button: provider icon, selected title,
|
||||
@@ -595,9 +470,8 @@ impl AgentSlide {
|
||||
}
|
||||
|
||||
/// Renders the vertical list of model rows shown inside the floating dropdown
|
||||
/// overlay. Each row: provider icon + title on the left, pill on the right
|
||||
/// (Premium for paywalled rows). Disabled rows are rendered dimmed and are
|
||||
/// not clickable or hover-selectable.
|
||||
/// overlay. Each row shows a provider icon + title on the left, with a
|
||||
/// "Recommended" pill on the right for the default model.
|
||||
fn render_model_list_rows(
|
||||
&self,
|
||||
appearance: &Appearance,
|
||||
@@ -609,7 +483,7 @@ impl AgentSlide {
|
||||
let state = self.onboarding_state.as_ref(app);
|
||||
let highlighted_id = self.highlighted_model_id.clone();
|
||||
let selected_id = state.agent_settings().selected_model_id.clone();
|
||||
let models = sorted_models(state.models());
|
||||
let models = state.models();
|
||||
|
||||
let mut col = Flex::column().with_cross_axis_alignment(CrossAxisAlignment::Stretch);
|
||||
|
||||
@@ -695,18 +569,11 @@ impl AgentSlide {
|
||||
let background_for_text = theme.background().into_solid();
|
||||
let ui_font_family = appearance.ui_font_family();
|
||||
|
||||
let is_disabled = model.requires_upgrade;
|
||||
|
||||
let title_color: ColorU = if is_disabled {
|
||||
internal_colors::text_disabled(theme, background_for_text)
|
||||
} else {
|
||||
internal_colors::text_main(theme, background_for_text)
|
||||
};
|
||||
let title_color: ColorU = internal_colors::text_main(theme, background_for_text);
|
||||
|
||||
let row_id = model.id.clone();
|
||||
let title = model.title.clone();
|
||||
let icon = model.icon;
|
||||
let requires_upgrade = model.requires_upgrade;
|
||||
let is_default = model.is_default;
|
||||
|
||||
let hoverable_body = Hoverable::new(mouse_state, move |_| {
|
||||
@@ -730,10 +597,7 @@ impl AgentSlide {
|
||||
.with_child(Container::new(title_el).with_margin_left(8.).finish())
|
||||
.finish();
|
||||
|
||||
// Trailing pills: "Recommended" on the server-designated default
|
||||
// model, "Premium" on paywalled rows. In practice a single row is
|
||||
// at most one of these, but both can be shown side-by-side if the
|
||||
// default is also premium for any reason.
|
||||
// "Recommended" pill on the server-designated default model.
|
||||
let make_pill = |label: &'static str| -> Box<dyn Element> {
|
||||
let badge = Text::new(label.to_string(), ui_font_family, 11.0)
|
||||
.with_color(internal_colors::text_sub(theme, background_for_text))
|
||||
@@ -755,8 +619,6 @@ impl AgentSlide {
|
||||
|
||||
let trailing: Box<dyn Element> = if is_default {
|
||||
make_pill("Recommended")
|
||||
} else if requires_upgrade {
|
||||
make_pill("Premium")
|
||||
} else {
|
||||
Empty::new().finish()
|
||||
};
|
||||
@@ -769,7 +631,7 @@ impl AgentSlide {
|
||||
.with_child(trailing)
|
||||
.finish();
|
||||
|
||||
let background = if is_highlighted && !is_disabled {
|
||||
let background = if is_highlighted {
|
||||
Some(Fill::Solid(internal_colors::neutral_2(theme)))
|
||||
} else {
|
||||
None
|
||||
@@ -787,26 +649,19 @@ impl AgentSlide {
|
||||
.finish()
|
||||
});
|
||||
|
||||
if is_disabled {
|
||||
// Disabled rows: no click, no hover-updates-highlight, muted.
|
||||
hoverable_body.finish()
|
||||
} else {
|
||||
let click_id = row_id.clone();
|
||||
let hover_id = row_id;
|
||||
hoverable_body
|
||||
.with_cursor(Cursor::PointingHand)
|
||||
.on_hover(move |is_hovered, ctx, _, _| {
|
||||
if is_hovered {
|
||||
ctx.dispatch_typed_action(AgentSlideAction::HighlightModel(
|
||||
hover_id.clone(),
|
||||
));
|
||||
}
|
||||
})
|
||||
.on_click(move |ctx, _, _| {
|
||||
ctx.dispatch_typed_action(AgentSlideAction::SelectModel(click_id.clone()));
|
||||
})
|
||||
.finish()
|
||||
}
|
||||
let click_id = row_id.clone();
|
||||
let hover_id = row_id;
|
||||
hoverable_body
|
||||
.with_cursor(Cursor::PointingHand)
|
||||
.on_hover(move |is_hovered, ctx, _, _| {
|
||||
if is_hovered {
|
||||
ctx.dispatch_typed_action(AgentSlideAction::HighlightModel(hover_id.clone()));
|
||||
}
|
||||
})
|
||||
.on_click(move |ctx, _, _| {
|
||||
ctx.dispatch_typed_action(AgentSlideAction::SelectModel(click_id.clone()));
|
||||
})
|
||||
.finish()
|
||||
}
|
||||
|
||||
fn render_autonomy_workspace_enforced(&self, appearance: &Appearance) -> Box<dyn Element> {
|
||||
@@ -881,19 +736,19 @@ impl AgentSlide {
|
||||
(
|
||||
AgentAutonomy::Full,
|
||||
"Full",
|
||||
"Runs commands, writes code, and reads files without asking.",
|
||||
"Warp Agent runs commands, writes code, and reads files without asking.",
|
||||
self.autonomy_full_mouse_state.clone(),
|
||||
),
|
||||
(
|
||||
AgentAutonomy::Partial,
|
||||
"Partial",
|
||||
"Can plan, read files, and execute low-risk commands. Asks before making any changes or executing sensitive commands.",
|
||||
"Warp Agent can plan, read files, and execute low-risk commands. Asks before making any changes or executing sensitive commands.",
|
||||
self.autonomy_partial_mouse_state.clone(),
|
||||
),
|
||||
(
|
||||
AgentAutonomy::None,
|
||||
"None",
|
||||
"Takes no actions without your approval.",
|
||||
"Warp Agent takes no actions without your approval.",
|
||||
self.autonomy_none_mouse_state.clone(),
|
||||
),
|
||||
];
|
||||
@@ -939,39 +794,7 @@ impl AgentSlide {
|
||||
.finish()
|
||||
}
|
||||
|
||||
fn render_disable_oz_section(
|
||||
&self,
|
||||
appearance: &Appearance,
|
||||
settings: &AgentDevelopmentSettings,
|
||||
) -> Box<dyn Element> {
|
||||
let theme = appearance.theme();
|
||||
let background_for_text = theme.background().into_solid();
|
||||
|
||||
let checkbox = appearance
|
||||
.ui_builder()
|
||||
.checkbox(self.disable_oz_mouse.clone(), Some(12.))
|
||||
.check(settings.disable_oz)
|
||||
.build()
|
||||
.on_click(|ctx, _, _| ctx.dispatch_typed_action(AgentSlideAction::ToggleDisableOz))
|
||||
.finish();
|
||||
|
||||
let label = Text::new("Disable Warp Agent", appearance.ui_font_family(), 14.0)
|
||||
.with_color(internal_colors::text_sub(theme, background_for_text))
|
||||
.with_style(Properties {
|
||||
weight: Weight::Normal,
|
||||
..Default::default()
|
||||
})
|
||||
.with_line_height_ratio(1.0)
|
||||
.finish();
|
||||
|
||||
Flex::row()
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Center)
|
||||
.with_child(checkbox)
|
||||
.with_child(Container::new(label).with_margin_left(8.).finish())
|
||||
.finish()
|
||||
}
|
||||
|
||||
fn render_bottom_nav(&self, appearance: &Appearance) -> Box<dyn Element> {
|
||||
fn render_bottom_nav(&self, appearance: &Appearance, app: &AppContext) -> Box<dyn Element> {
|
||||
let back_button = self.back_button.render(
|
||||
appearance,
|
||||
button::Params {
|
||||
@@ -1002,13 +825,7 @@ impl AgentSlide {
|
||||
},
|
||||
);
|
||||
|
||||
let step_index = 2;
|
||||
let step_count =
|
||||
if galaxy_core::features::FeatureFlag::OpenWarpNewSettingsModes.is_enabled() {
|
||||
5
|
||||
} else {
|
||||
4
|
||||
};
|
||||
let (step_index, step_count) = self.onboarding_state.as_ref(app).progress();
|
||||
bottom_nav::onboarding_bottom_nav(
|
||||
appearance,
|
||||
step_index,
|
||||
@@ -1018,98 +835,6 @@ impl AgentSlide {
|
||||
)
|
||||
}
|
||||
|
||||
fn render_upgrade_banner(&self, appearance: &Appearance) -> Box<dyn Element> {
|
||||
// Diagonal magenta → yellow gradient (top-left to bottom-right). Chosen
|
||||
// to match the "premium" glow styling in the Figma mocks.
|
||||
const GRADIENT_START_MAGENTA: ColorU = ColorU {
|
||||
r: 0xE2,
|
||||
g: 0x48,
|
||||
b: 0xBC,
|
||||
a: 0xFF,
|
||||
};
|
||||
const GRADIENT_END_YELLOW: ColorU = ColorU {
|
||||
r: 0xF5,
|
||||
g: 0xB7,
|
||||
b: 0x00,
|
||||
a: 0xFF,
|
||||
};
|
||||
|
||||
let theme = appearance.theme();
|
||||
let background_for_text = theme.background().into_solid();
|
||||
let ui_font_family = appearance.ui_font_family();
|
||||
|
||||
// Primary "heading" line: bolder, full-contrast.
|
||||
let title = Text::new(
|
||||
"Upgrade for access to premium models.",
|
||||
ui_font_family,
|
||||
13.0,
|
||||
)
|
||||
.with_color(internal_colors::text_main(theme, background_for_text))
|
||||
.with_style(Properties {
|
||||
weight: Weight::Medium,
|
||||
..Default::default()
|
||||
})
|
||||
.with_line_height_ratio(1.2)
|
||||
.finish();
|
||||
|
||||
// Secondary subtext: muted, normal weight.
|
||||
let subtitle = Text::new(
|
||||
"State-of-the-art models require paid plans.",
|
||||
ui_font_family,
|
||||
12.0,
|
||||
)
|
||||
.with_color(internal_colors::text_sub(theme, background_for_text))
|
||||
.with_style(Properties {
|
||||
weight: Weight::Normal,
|
||||
..Default::default()
|
||||
})
|
||||
.with_line_height_ratio(1.2)
|
||||
.finish();
|
||||
|
||||
let text_col = Flex::column()
|
||||
.with_main_axis_size(MainAxisSize::Min)
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Start)
|
||||
.with_child(title)
|
||||
.with_child(Container::new(subtitle).with_margin_top(4.).finish())
|
||||
.finish();
|
||||
|
||||
let upgrade_button = self.upgrade_button.render(
|
||||
appearance,
|
||||
button::Params {
|
||||
content: button::Content::Label("Upgrade".into()),
|
||||
theme: &UpgradeButtonTheme,
|
||||
options: button::Options {
|
||||
size: button::Size::Small,
|
||||
on_click: Some(Box::new(|ctx, _app, _pos| {
|
||||
ctx.dispatch_typed_action(AgentSlideAction::UpgradeClicked);
|
||||
})),
|
||||
..button::Options::default(appearance)
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
let row = Flex::row()
|
||||
.with_main_axis_size(MainAxisSize::Max)
|
||||
.with_main_axis_alignment(MainAxisAlignment::SpaceBetween)
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Center)
|
||||
.with_child(text_col)
|
||||
.with_child(upgrade_button)
|
||||
.finish();
|
||||
|
||||
Container::new(row)
|
||||
.with_uniform_padding(12.)
|
||||
.with_corner_radius(CornerRadius::with_all(Radius::Pixels(8.)))
|
||||
.with_border(Border::all(1.).with_border_gradient(
|
||||
vec2f(0., 0.),
|
||||
vec2f(1., 1.),
|
||||
Gradient {
|
||||
start: GRADIENT_START_MAGENTA,
|
||||
end: GRADIENT_END_YELLOW,
|
||||
},
|
||||
))
|
||||
.finish()
|
||||
}
|
||||
|
||||
fn render_visual(&self, appearance: &Appearance, app: &AppContext) -> Box<dyn Element> {
|
||||
let theme = appearance.theme();
|
||||
|
||||
@@ -1138,187 +863,10 @@ impl AgentSlide {
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// Full-width bar pinned below the slide's two-column layout. Shown after
|
||||
/// the user clicks the Upgrade button, so they can fall back to copying
|
||||
/// the upgrade URL (or pasting the returned auth token) if the browser
|
||||
/// didn't launch automatically.
|
||||
fn render_auth_prompt_bar(&self, appearance: &Appearance) -> Box<dyn Element> {
|
||||
const BAR_HEIGHT: f32 = 40.;
|
||||
const ICON_SIZE: f32 = 14.;
|
||||
const FONT_SIZE: f32 = 12.;
|
||||
|
||||
let theme = appearance.theme();
|
||||
let bar_bg = theme.surface_1();
|
||||
let bar_bg_solid = bar_bg.into_solid();
|
||||
let text_color = internal_colors::text_sub(theme, bar_bg_solid);
|
||||
let ui_builder = appearance.ui_builder();
|
||||
|
||||
let text_styles = UiComponentStyles {
|
||||
font_color: Some(text_color),
|
||||
font_size: Some(FONT_SIZE),
|
||||
..Default::default()
|
||||
};
|
||||
let link_styles = UiComponentStyles {
|
||||
font_size: Some(FONT_SIZE),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let icon = ConstrainedBox::new(Box::new(
|
||||
Icon::AlertCircle.to_galaxyui_icon(Fill::Solid(text_color)),
|
||||
))
|
||||
.with_width(ICON_SIZE)
|
||||
.with_height(ICON_SIZE)
|
||||
.finish();
|
||||
|
||||
let copy_url_link = ui_builder
|
||||
.link(
|
||||
"copy the URL".into(),
|
||||
None,
|
||||
Some(Box::new(|ctx| {
|
||||
ctx.dispatch_typed_action(AgentSlideAction::CopyUpgradeUrlClicked);
|
||||
})),
|
||||
self.copy_url_mouse_state.clone(),
|
||||
)
|
||||
.soft_wrap(false)
|
||||
.with_style(link_styles)
|
||||
.build()
|
||||
.finish();
|
||||
|
||||
let paste_token_link = ui_builder
|
||||
.link(
|
||||
"Click here".into(),
|
||||
None,
|
||||
Some(Box::new(|ctx| {
|
||||
ctx.dispatch_typed_action(AgentSlideAction::PasteAuthTokenFromClipboardClicked);
|
||||
})),
|
||||
self.paste_token_mouse_state.clone(),
|
||||
)
|
||||
.soft_wrap(false)
|
||||
.with_style(link_styles)
|
||||
.build()
|
||||
.finish();
|
||||
|
||||
let text_row = Flex::row()
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Center)
|
||||
.with_child(icon)
|
||||
.with_child(
|
||||
Container::new(
|
||||
ui_builder
|
||||
.span("If your browser hasn't launched, ")
|
||||
.with_style(text_styles)
|
||||
.build()
|
||||
.finish(),
|
||||
)
|
||||
.with_margin_left(8.)
|
||||
.finish(),
|
||||
)
|
||||
.with_child(copy_url_link)
|
||||
.with_child(
|
||||
ui_builder
|
||||
.span(" and open the page manually. ")
|
||||
.with_style(text_styles)
|
||||
.build()
|
||||
.finish(),
|
||||
)
|
||||
.with_child(paste_token_link)
|
||||
.with_child(
|
||||
ui_builder
|
||||
.span(" to paste your token from the browser.")
|
||||
.with_style(text_styles)
|
||||
.build()
|
||||
.finish(),
|
||||
)
|
||||
.finish();
|
||||
|
||||
let row = Flex::row()
|
||||
.with_main_axis_size(MainAxisSize::Max)
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Center)
|
||||
.with_child(text_row)
|
||||
.finish();
|
||||
|
||||
ConstrainedBox::new(
|
||||
Container::new(row)
|
||||
.with_background(bar_bg)
|
||||
.with_border(Border::top(1.).with_border_color(internal_colors::neutral_4(theme)))
|
||||
.with_horizontal_padding(16.)
|
||||
.finish(),
|
||||
)
|
||||
.with_min_height(BAR_HEIGHT)
|
||||
.finish()
|
||||
}
|
||||
|
||||
/// Green success pill shown when the user's `OnboardingAuthState`
|
||||
/// transitions into `PayingUser`. Auto-dismisses after
|
||||
/// `PLAN_ACTIVATED_TOAST_DURATION`; also dismissable via the close X.
|
||||
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. All premium models are available.")
|
||||
.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(AgentSlideAction::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()
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for AgentSlide {
|
||||
type Event = AgentSlideEvent;
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl View for AgentSlide {
|
||||
@@ -1334,39 +882,10 @@ impl View for AgentSlide {
|
||||
// The floating dropdown overlay is built inside `render_model_section`
|
||||
// so it inherits the column width naturally. Here we only need the
|
||||
// base two-column layout.
|
||||
let slide = layout::static_left(
|
||||
layout::static_left(
|
||||
|| self.render_content(appearance, settings, workspace_enforces_autonomy, app),
|
||||
|| self.render_visual(appearance, app),
|
||||
);
|
||||
|
||||
// Upgrade-prompt bar: shown after the user clicks Upgrade, as long as
|
||||
// they aren't yet on a paid plan. Overlays the bottom of the slide
|
||||
// (doesn't bump slide content up) so the slide layout stays stable
|
||||
// whether or not the bar is visible.
|
||||
//
|
||||
// The plan-activated success toast supersedes the bar (and any other
|
||||
// bottom overlay) while it's visible.
|
||||
let auth_state = self.onboarding_state.as_ref(app).auth_state();
|
||||
let show_bar =
|
||||
self.show_auth_prompt_bar && !matches!(auth_state, OnboardingAuthState::PayingUser);
|
||||
if !show_bar && !self.show_plan_activated_toast {
|
||||
return slide;
|
||||
}
|
||||
|
||||
let bottom_overlay = if self.show_plan_activated_toast {
|
||||
self.render_plan_activated_toast(appearance)
|
||||
} else {
|
||||
self.render_auth_prompt_bar(appearance)
|
||||
};
|
||||
|
||||
let mut stack = Stack::new();
|
||||
stack.add_child(slide);
|
||||
stack.add_child(
|
||||
galaxyui::elements::Align::new(bottom_overlay)
|
||||
.bottom_center()
|
||||
.finish(),
|
||||
);
|
||||
stack.finish()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1388,10 +907,7 @@ impl AgentSlide {
|
||||
// starts on the selected row.
|
||||
let state = self.onboarding_state.as_ref(ctx);
|
||||
let selected_id = state.agent_settings().selected_model_id.clone();
|
||||
if let Some(index) = sorted_models(state.models())
|
||||
.iter()
|
||||
.position(|m| m.id == selected_id)
|
||||
{
|
||||
if let Some(index) = state.models().iter().position(|m| m.id == selected_id) {
|
||||
self.dropdown_scroll_state.scroll_to_position(ScrollTarget {
|
||||
position_id: model_row_position_id(index),
|
||||
mode: ScrollToPositionMode::FullyIntoView,
|
||||
@@ -1402,55 +918,33 @@ impl AgentSlide {
|
||||
ctx.notify();
|
||||
}
|
||||
|
||||
/// Finds the next enabled model index in the given direction, wrapping
|
||||
/// around. Indices are into the slide's sorted view of the model list.
|
||||
/// Returns `None` if all models are paywalled.
|
||||
fn next_enabled_model_index(
|
||||
&self,
|
||||
start: usize,
|
||||
forward: bool,
|
||||
ctx: &AppContext,
|
||||
) -> Option<usize> {
|
||||
let models = sorted_models(self.onboarding_state.as_ref(ctx).models());
|
||||
let count = models.len();
|
||||
if count == 0 {
|
||||
return None;
|
||||
}
|
||||
for offset in 1..=count {
|
||||
let idx = if forward {
|
||||
(start + offset) % count
|
||||
} else {
|
||||
(start + count - offset) % count
|
||||
};
|
||||
if !models[idx].requires_upgrade {
|
||||
return Some(idx);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Advances the highlight cursor to the next/previous enabled model, wrapping.
|
||||
/// The origin of the walk is the currently-highlighted id (if any), else the
|
||||
/// Advances the highlight cursor to the next/previous model, wrapping. The
|
||||
/// origin of the walk is the currently-highlighted id (if any), else the
|
||||
/// currently-selected id. Also scrolls the dropdown so the newly-highlighted
|
||||
/// row stays visible — same `SavePosition` + `scroll_to_position` pattern
|
||||
/// used by `VerticalTabsPanelState::scroll_to_tab`.
|
||||
fn advance_highlighted_model(&mut self, forward: bool, ctx: &mut ViewContext<Self>) {
|
||||
let state = self.onboarding_state.as_ref(ctx);
|
||||
let sorted = sorted_models(state.models());
|
||||
let selected_id = state.agent_settings().selected_model_id.clone();
|
||||
let (model_ids, selected_id) = {
|
||||
let state = self.onboarding_state.as_ref(ctx);
|
||||
let ids: Vec<LLMId> = state.models().iter().map(|m| m.id.clone()).collect();
|
||||
(ids, state.agent_settings().selected_model_id.clone())
|
||||
};
|
||||
let count = model_ids.len();
|
||||
if count == 0 {
|
||||
return;
|
||||
}
|
||||
let start_index = self
|
||||
.highlighted_model_id
|
||||
.as_ref()
|
||||
.and_then(|id| sorted.iter().position(|m| &m.id == id))
|
||||
.or_else(|| sorted.iter().position(|m| m.id == selected_id))
|
||||
.and_then(|id| model_ids.iter().position(|m| m == id))
|
||||
.or_else(|| model_ids.iter().position(|m| *m == selected_id))
|
||||
.unwrap_or(0);
|
||||
let Some(next_index) = self.next_enabled_model_index(start_index, forward, ctx) else {
|
||||
return;
|
||||
let next_index = if forward {
|
||||
(start_index + 1) % count
|
||||
} else {
|
||||
(start_index + count - 1) % count
|
||||
};
|
||||
let Some(next_id) = sorted.get(next_index).map(|m| m.id.clone()) else {
|
||||
return;
|
||||
};
|
||||
self.highlighted_model_id = Some(next_id);
|
||||
self.highlighted_model_id = Some(model_ids[next_index].clone());
|
||||
// Scroll the dropdown so the new highlight is visible. `FullyIntoView`
|
||||
// is a no-op when the row is already fully in view, otherwise it
|
||||
// scrolls the minimum amount to show it.
|
||||
@@ -1532,16 +1026,7 @@ impl OnboardingSlide for AgentSlide {
|
||||
// and collapses the list. Does NOT advance to the next slide.
|
||||
if self.is_model_list_expanded {
|
||||
if let Some(id) = self.highlighted_model_id.clone() {
|
||||
// Only select if the highlighted row is still enabled.
|
||||
let enabled = self
|
||||
.onboarding_state
|
||||
.as_ref(ctx)
|
||||
.models()
|
||||
.iter()
|
||||
.any(|m| m.id == id && !m.requires_upgrade);
|
||||
if enabled {
|
||||
self.select_model(id, ctx);
|
||||
}
|
||||
self.select_model(id, ctx);
|
||||
}
|
||||
self.set_model_list_expanded(false, ctx);
|
||||
return;
|
||||
@@ -1578,16 +1063,7 @@ impl TypedActionView for AgentSlide {
|
||||
self.set_model_list_expanded(!self.is_model_list_expanded, ctx);
|
||||
}
|
||||
AgentSlideAction::HighlightModel(model_id) => {
|
||||
// Only update if the id corresponds to an enabled row. Callers
|
||||
// (hover handlers) already filter this out, but we defend against
|
||||
// stale actions fired while the list was re-rendering.
|
||||
let enabled = self
|
||||
.onboarding_state
|
||||
.as_ref(ctx)
|
||||
.models()
|
||||
.iter()
|
||||
.any(|m| m.id == *model_id && !m.requires_upgrade);
|
||||
if enabled && self.highlighted_model_id.as_ref() != Some(model_id) {
|
||||
if self.highlighted_model_id.as_ref() != Some(model_id) {
|
||||
self.highlighted_model_id = Some(model_id.clone());
|
||||
ctx.notify();
|
||||
}
|
||||
@@ -1616,29 +1092,6 @@ impl TypedActionView for AgentSlide {
|
||||
AgentSlideAction::NextClicked => {
|
||||
self.next(ctx);
|
||||
}
|
||||
AgentSlideAction::UpgradeClicked => {
|
||||
send_telemetry_from_ctx!(OnboardingEvent::AgentSlideUpgradeClicked, ctx);
|
||||
if !matches!(
|
||||
self.onboarding_state.as_ref(ctx).auth_state(),
|
||||
OnboardingAuthState::PayingUser,
|
||||
) {
|
||||
self.show_auth_prompt_bar = true;
|
||||
ctx.notify();
|
||||
}
|
||||
self.onboarding_state.update(ctx, |state, ctx| {
|
||||
state.request_upgrade(ctx);
|
||||
});
|
||||
}
|
||||
AgentSlideAction::CopyUpgradeUrlClicked => {
|
||||
ctx.emit(AgentSlideEvent::CopyUpgradeUrlRequested);
|
||||
}
|
||||
AgentSlideAction::PasteAuthTokenFromClipboardClicked => {
|
||||
ctx.emit(AgentSlideEvent::PasteAuthTokenFromClipboardRequested);
|
||||
}
|
||||
AgentSlideAction::DismissPlanActivatedToast => {
|
||||
self.show_plan_activated_toast = false;
|
||||
ctx.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,622 @@
|
||||
use ui_components::{button, Component as _, Options as _};
|
||||
use galaxy_core::ui::appearance::Appearance;
|
||||
use galaxy_core::ui::icons::Icon;
|
||||
use galaxy_core::ui::theme::color::internal_colors;
|
||||
use galaxy_core::ui::theme::Fill;
|
||||
use galaxyui_core::elements::{
|
||||
Border, ClippedScrollStateHandle, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment,
|
||||
Flex, FormattedTextElement, Hoverable, MainAxisAlignment, MainAxisSize, MouseStateHandle,
|
||||
ParentElement, Radius, Stack,
|
||||
};
|
||||
use galaxyui_core::fonts::Weight;
|
||||
use galaxyui_core::keymap::Keystroke;
|
||||
use galaxyui_core::platform::Cursor;
|
||||
use galaxyui_core::prelude::Align;
|
||||
use galaxyui_core::text_layout::TextAlignment;
|
||||
use galaxyui_core::ui_components::components::{UiComponent as _, UiComponentStyles};
|
||||
use galaxyui_core::{
|
||||
AppContext, Element, Entity, ModelHandle, SingletonEntity as _, TypedActionView, View,
|
||||
ViewContext,
|
||||
};
|
||||
|
||||
use super::OnboardingSlide;
|
||||
use crate::model::{AiAccessChoice, OnboardingAuthState, OnboardingStateModel};
|
||||
use crate::slides::{bottom_nav, layout, slide_content};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum AiAccessSlideAction {
|
||||
SelectSubscription,
|
||||
SelectSetUpLater,
|
||||
CopyUpgradeUrlClicked,
|
||||
PasteAuthTokenFromClipboardClicked,
|
||||
BackClicked,
|
||||
NextClicked,
|
||||
}
|
||||
|
||||
/// Emitted to the parent onboarding view so the (app-crate) upgrade fallback
|
||||
/// actions can be handled at the root level — the onboarding crate can't
|
||||
/// reference them directly.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum AiAccessSlideEvent {
|
||||
CopyUpgradeUrlRequested,
|
||||
PasteAuthTokenFromClipboardRequested,
|
||||
}
|
||||
|
||||
/// The "Choose how to access AI" slide (Warp Agent path). Forks between a paid
|
||||
/// subscription and a "Set up later" option that lets the user explore Warp's
|
||||
/// built-in AI before committing to a plan.
|
||||
pub struct AiAccessSlide {
|
||||
onboarding_state: ModelHandle<OnboardingStateModel>,
|
||||
subscription_mouse_state: MouseStateHandle,
|
||||
set_up_later_mouse_state: MouseStateHandle,
|
||||
back_button: button::Button,
|
||||
next_button: button::Button,
|
||||
scroll_state: ClippedScrollStateHandle,
|
||||
show_auth_prompt_bar: bool,
|
||||
copy_url_mouse_state: MouseStateHandle,
|
||||
paste_token_mouse_state: MouseStateHandle,
|
||||
}
|
||||
|
||||
impl AiAccessSlide {
|
||||
pub(crate) fn new(onboarding_state: ModelHandle<OnboardingStateModel>) -> Self {
|
||||
Self {
|
||||
onboarding_state,
|
||||
subscription_mouse_state: MouseStateHandle::default(),
|
||||
set_up_later_mouse_state: MouseStateHandle::default(),
|
||||
back_button: button::Button::default(),
|
||||
next_button: button::Button::default(),
|
||||
scroll_state: ClippedScrollStateHandle::new(),
|
||||
show_auth_prompt_bar: false,
|
||||
copy_url_mouse_state: MouseStateHandle::default(),
|
||||
paste_token_mouse_state: MouseStateHandle::default(),
|
||||
}
|
||||
}
|
||||
|
||||
// The final DES-816 visual exports have not landed yet, so the right panel
|
||||
// reuses the existing bundled agent welcome image.
|
||||
pub(crate) const VISUAL_IMAGE_PATHS: &'static [&'static str] =
|
||||
&["async/png/onboarding/welcome_agent.png"];
|
||||
|
||||
fn choice(&self, app: &AppContext) -> AiAccessChoice {
|
||||
self.onboarding_state.as_ref(app).ai_access_choice()
|
||||
}
|
||||
|
||||
fn render_content(
|
||||
&self,
|
||||
appearance: &Appearance,
|
||||
choice: AiAccessChoice,
|
||||
app: &AppContext,
|
||||
) -> Box<dyn Element> {
|
||||
let bottom_nav = Align::new(self.render_bottom_nav(appearance, app)).finish();
|
||||
|
||||
slide_content::onboarding_slide_content(
|
||||
vec![
|
||||
Align::new(self.render_header(appearance)).left().finish(),
|
||||
Align::new(self.render_options(appearance, choice)).finish(),
|
||||
],
|
||||
bottom_nav,
|
||||
self.scroll_state.clone(),
|
||||
appearance,
|
||||
)
|
||||
}
|
||||
|
||||
fn render_header(&self, appearance: &Appearance) -> Box<dyn Element> {
|
||||
let theme = appearance.theme();
|
||||
|
||||
let title = appearance
|
||||
.ui_builder()
|
||||
.paragraph("Get AI access")
|
||||
.with_style(UiComponentStyles {
|
||||
font_size: Some(36.),
|
||||
font_weight: Some(Weight::Medium),
|
||||
..Default::default()
|
||||
})
|
||||
.build()
|
||||
.finish();
|
||||
|
||||
let subtitle = FormattedTextElement::from_str(
|
||||
"Save with a recurring plan, or explore Warp's AI before committing.",
|
||||
appearance.ui_font_family(),
|
||||
16.,
|
||||
)
|
||||
.with_color(internal_colors::text_sub(
|
||||
theme,
|
||||
theme.background().into_solid(),
|
||||
))
|
||||
.with_weight(Weight::Normal)
|
||||
.with_alignment(TextAlignment::Left)
|
||||
.with_line_height_ratio(1.0)
|
||||
.finish();
|
||||
|
||||
Flex::column()
|
||||
.with_main_axis_size(MainAxisSize::Min)
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Start)
|
||||
.with_child(title)
|
||||
.with_child(Container::new(subtitle).with_margin_top(16.).finish())
|
||||
.finish()
|
||||
}
|
||||
|
||||
fn render_options(&self, appearance: &Appearance, choice: AiAccessChoice) -> Box<dyn Element> {
|
||||
let subscription_card = self
|
||||
.render_subscription_card(appearance, matches!(choice, AiAccessChoice::Subscription));
|
||||
|
||||
let set_up_later_card =
|
||||
self.render_set_up_later_card(appearance, matches!(choice, AiAccessChoice::SetUpLater));
|
||||
|
||||
Container::new(
|
||||
Flex::column()
|
||||
.with_main_axis_size(MainAxisSize::Min)
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Stretch)
|
||||
.with_child(
|
||||
Container::new(subscription_card)
|
||||
.with_margin_bottom(12.)
|
||||
.finish(),
|
||||
)
|
||||
.with_child(set_up_later_card)
|
||||
.finish(),
|
||||
)
|
||||
.with_margin_top(38.)
|
||||
.finish()
|
||||
}
|
||||
|
||||
/// Shared chrome for an option card: selected/unselected background + border,
|
||||
/// hover/click to select.
|
||||
fn render_card_chrome(
|
||||
appearance: &Appearance,
|
||||
is_selected: bool,
|
||||
mouse_state: MouseStateHandle,
|
||||
select_action: AiAccessSlideAction,
|
||||
content: Box<dyn Element>,
|
||||
) -> Box<dyn Element> {
|
||||
const RADIUS: f32 = 8.;
|
||||
|
||||
let theme = appearance.theme();
|
||||
let background = if is_selected {
|
||||
Some(internal_colors::accent_overlay_1(theme))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let border_color = if is_selected {
|
||||
theme.accent()
|
||||
} else {
|
||||
Fill::Solid(internal_colors::neutral_4(theme))
|
||||
};
|
||||
|
||||
Hoverable::new(mouse_state, move |_| {
|
||||
let mut container = Container::new(content)
|
||||
.with_uniform_padding(24.)
|
||||
.with_corner_radius(CornerRadius::with_all(Radius::Pixels(RADIUS)))
|
||||
.with_border(Border::all(1.).with_border_fill(border_color));
|
||||
if let Some(bg) = background {
|
||||
container = container.with_background(bg);
|
||||
}
|
||||
container.finish()
|
||||
})
|
||||
.with_cursor(Cursor::PointingHand)
|
||||
.on_click(move |ctx, _, _| {
|
||||
ctx.dispatch_typed_action(select_action.clone());
|
||||
})
|
||||
.finish()
|
||||
}
|
||||
|
||||
fn render_subscription_card(
|
||||
&self,
|
||||
appearance: &Appearance,
|
||||
is_selected: bool,
|
||||
) -> Box<dyn Element> {
|
||||
let theme = appearance.theme();
|
||||
let bg_solid = theme.background().into_solid();
|
||||
let label_color = if is_selected {
|
||||
internal_colors::text_main(theme, bg_solid)
|
||||
} else {
|
||||
internal_colors::text_sub(theme, bg_solid)
|
||||
};
|
||||
let description_color = internal_colors::text_sub(theme, bg_solid);
|
||||
|
||||
let label = appearance
|
||||
.ui_builder()
|
||||
.paragraph("Subscription")
|
||||
.with_style(UiComponentStyles {
|
||||
font_size: Some(16.),
|
||||
font_weight: Some(Weight::Semibold),
|
||||
font_color: Some(label_color),
|
||||
..Default::default()
|
||||
})
|
||||
.build()
|
||||
.finish();
|
||||
|
||||
let badge = {
|
||||
let green = theme.ansi_fg_green();
|
||||
let badge_text = appearance
|
||||
.ui_builder()
|
||||
.paragraph("Best value")
|
||||
.with_style(UiComponentStyles {
|
||||
font_size: Some(12.),
|
||||
font_weight: Some(Weight::Normal),
|
||||
font_color: Some(green),
|
||||
..Default::default()
|
||||
})
|
||||
.build()
|
||||
.finish();
|
||||
Container::new(badge_text)
|
||||
.with_corner_radius(CornerRadius::with_all(Radius::Pixels(11.)))
|
||||
.with_border(Border::all(1.).with_border_fill(Fill::Solid(green)))
|
||||
.with_horizontal_padding(8.)
|
||||
.with_vertical_padding(3.)
|
||||
.finish()
|
||||
};
|
||||
|
||||
let header_row = Flex::row()
|
||||
.with_main_axis_size(MainAxisSize::Max)
|
||||
.with_main_axis_alignment(MainAxisAlignment::SpaceBetween)
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Center)
|
||||
.with_child(label)
|
||||
.with_child(badge)
|
||||
.finish();
|
||||
|
||||
let description = FormattedTextElement::from_str(
|
||||
"Starting at $18 / mo, available with monthly or annual plans. Includes base credits, \
|
||||
frontier models, cloud agents, collaboration, and more.",
|
||||
appearance.ui_font_family(),
|
||||
14.,
|
||||
)
|
||||
.with_color(description_color)
|
||||
.with_weight(Weight::Normal)
|
||||
.with_alignment(TextAlignment::Left)
|
||||
.with_line_height_ratio(1.2)
|
||||
.finish();
|
||||
|
||||
let content = Flex::column()
|
||||
.with_main_axis_size(MainAxisSize::Min)
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Start)
|
||||
.with_child(header_row)
|
||||
.with_child(Container::new(description).with_margin_top(12.).finish())
|
||||
.finish();
|
||||
|
||||
Self::render_card_chrome(
|
||||
appearance,
|
||||
is_selected,
|
||||
self.subscription_mouse_state.clone(),
|
||||
AiAccessSlideAction::SelectSubscription,
|
||||
content,
|
||||
)
|
||||
}
|
||||
|
||||
fn render_set_up_later_card(
|
||||
&self,
|
||||
appearance: &Appearance,
|
||||
is_selected: bool,
|
||||
) -> Box<dyn Element> {
|
||||
let theme = appearance.theme();
|
||||
let bg_solid = theme.background().into_solid();
|
||||
let label_color = if is_selected {
|
||||
internal_colors::text_main(theme, bg_solid)
|
||||
} else {
|
||||
internal_colors::text_sub(theme, bg_solid)
|
||||
};
|
||||
let description_color = internal_colors::text_sub(theme, bg_solid);
|
||||
|
||||
let label = appearance
|
||||
.ui_builder()
|
||||
.paragraph("Set up later")
|
||||
.with_style(UiComponentStyles {
|
||||
font_size: Some(16.),
|
||||
font_weight: Some(Weight::Semibold),
|
||||
font_color: Some(label_color),
|
||||
..Default::default()
|
||||
})
|
||||
.build()
|
||||
.finish();
|
||||
|
||||
let description = FormattedTextElement::from_str(
|
||||
"Explore Warp's built-in AI features before committing to a plan, or bring your own \
|
||||
inference.",
|
||||
appearance.ui_font_family(),
|
||||
14.,
|
||||
)
|
||||
.with_color(description_color)
|
||||
.with_weight(Weight::Normal)
|
||||
.with_alignment(TextAlignment::Left)
|
||||
.with_line_height_ratio(1.2)
|
||||
.finish();
|
||||
|
||||
let content = Flex::column()
|
||||
.with_main_axis_size(MainAxisSize::Min)
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Start)
|
||||
.with_child(label)
|
||||
.with_child(Container::new(description).with_margin_top(12.).finish())
|
||||
.finish();
|
||||
|
||||
Self::render_card_chrome(
|
||||
appearance,
|
||||
is_selected,
|
||||
self.set_up_later_mouse_state.clone(),
|
||||
AiAccessSlideAction::SelectSetUpLater,
|
||||
content,
|
||||
)
|
||||
}
|
||||
|
||||
fn render_bottom_nav(&self, appearance: &Appearance, app: &AppContext) -> Box<dyn Element> {
|
||||
let back_button = self.back_button.render(
|
||||
appearance,
|
||||
button::Params {
|
||||
content: button::Content::Label("Back".into()),
|
||||
theme: &button::themes::Naked,
|
||||
options: button::Options {
|
||||
on_click: Some(Box::new(|ctx, _app, _pos| {
|
||||
ctx.dispatch_typed_action(AiAccessSlideAction::BackClicked);
|
||||
})),
|
||||
..button::Options::default(appearance)
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
let enter = Keystroke::parse("enter").unwrap_or_default();
|
||||
let next_button = self.next_button.render(
|
||||
appearance,
|
||||
button::Params {
|
||||
content: button::Content::Label("Next".into()),
|
||||
theme: &button::themes::Primary,
|
||||
options: button::Options {
|
||||
keystroke: Some(enter),
|
||||
on_click: Some(Box::new(|ctx, _app, _pos| {
|
||||
ctx.dispatch_typed_action(AiAccessSlideAction::NextClicked);
|
||||
})),
|
||||
..button::Options::default(appearance)
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
let (step_index, step_count) = self.onboarding_state.as_ref(app).progress();
|
||||
bottom_nav::onboarding_bottom_nav(
|
||||
appearance,
|
||||
step_index,
|
||||
step_count,
|
||||
Some(back_button),
|
||||
Some(next_button),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_visual(&self) -> Box<dyn Element> {
|
||||
layout::onboarding_right_panel_with_bg(
|
||||
Self::VISUAL_IMAGE_PATHS[0],
|
||||
layout::FOREGROUND_LAYOUT_DEFAULT,
|
||||
)
|
||||
}
|
||||
|
||||
/// Full-width bar pinned below the slide's two-column layout. Shown after
|
||||
/// the user picks Subscription and clicks "Next", so they can fall back to
|
||||
/// copying the upgrade URL (or pasting the returned auth token) if the
|
||||
/// browser didn't launch automatically.
|
||||
fn render_auth_prompt_bar(&self, appearance: &Appearance) -> Box<dyn Element> {
|
||||
const BAR_HEIGHT: f32 = 40.;
|
||||
const ICON_SIZE: f32 = 14.;
|
||||
const FONT_SIZE: f32 = 12.;
|
||||
|
||||
let theme = appearance.theme();
|
||||
let bar_bg = theme.surface_1();
|
||||
let bar_bg_solid = bar_bg.into_solid();
|
||||
let text_color = internal_colors::text_sub(theme, bar_bg_solid);
|
||||
let ui_builder = appearance.ui_builder();
|
||||
|
||||
let text_styles = UiComponentStyles {
|
||||
font_color: Some(text_color),
|
||||
font_size: Some(FONT_SIZE),
|
||||
..Default::default()
|
||||
};
|
||||
let link_styles = UiComponentStyles {
|
||||
font_size: Some(FONT_SIZE),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let icon = ConstrainedBox::new(Box::new(
|
||||
Icon::AlertCircle.to_warpui_icon(Fill::Solid(text_color)),
|
||||
))
|
||||
.with_width(ICON_SIZE)
|
||||
.with_height(ICON_SIZE)
|
||||
.finish();
|
||||
|
||||
let copy_url_link = ui_builder
|
||||
.link(
|
||||
"copy the URL".into(),
|
||||
None,
|
||||
Some(Box::new(|ctx| {
|
||||
ctx.dispatch_typed_action(AiAccessSlideAction::CopyUpgradeUrlClicked);
|
||||
})),
|
||||
self.copy_url_mouse_state.clone(),
|
||||
)
|
||||
.soft_wrap(false)
|
||||
.with_style(link_styles)
|
||||
.build()
|
||||
.finish();
|
||||
|
||||
let paste_token_link = ui_builder
|
||||
.link(
|
||||
"Click here".into(),
|
||||
None,
|
||||
Some(Box::new(|ctx| {
|
||||
ctx.dispatch_typed_action(
|
||||
AiAccessSlideAction::PasteAuthTokenFromClipboardClicked,
|
||||
);
|
||||
})),
|
||||
self.paste_token_mouse_state.clone(),
|
||||
)
|
||||
.soft_wrap(false)
|
||||
.with_style(link_styles)
|
||||
.build()
|
||||
.finish();
|
||||
|
||||
let text_row = Flex::row()
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Center)
|
||||
.with_child(icon)
|
||||
.with_child(
|
||||
Container::new(
|
||||
ui_builder
|
||||
.span("If your browser hasn't launched, ")
|
||||
.with_style(text_styles)
|
||||
.build()
|
||||
.finish(),
|
||||
)
|
||||
.with_margin_left(8.)
|
||||
.finish(),
|
||||
)
|
||||
.with_child(copy_url_link)
|
||||
.with_child(
|
||||
ui_builder
|
||||
.span(" and open the page manually. ")
|
||||
.with_style(text_styles)
|
||||
.build()
|
||||
.finish(),
|
||||
)
|
||||
.with_child(paste_token_link)
|
||||
.with_child(
|
||||
ui_builder
|
||||
.span(" to paste your token from the browser.")
|
||||
.with_style(text_styles)
|
||||
.build()
|
||||
.finish(),
|
||||
)
|
||||
.finish();
|
||||
|
||||
let row = Flex::row()
|
||||
.with_main_axis_size(MainAxisSize::Max)
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Center)
|
||||
.with_child(text_row)
|
||||
.finish();
|
||||
|
||||
ConstrainedBox::new(
|
||||
Container::new(row)
|
||||
.with_background(bar_bg)
|
||||
.with_border(Border::top(1.).with_border_color(internal_colors::neutral_4(theme)))
|
||||
.with_horizontal_padding(16.)
|
||||
.finish(),
|
||||
)
|
||||
.with_min_height(BAR_HEIGHT)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for AiAccessSlide {
|
||||
type Event = AiAccessSlideEvent;
|
||||
}
|
||||
|
||||
impl View for AiAccessSlide {
|
||||
fn ui_name() -> &'static str {
|
||||
"AiAccessSlide"
|
||||
}
|
||||
|
||||
fn render(&self, app: &AppContext) -> Box<dyn Element> {
|
||||
let appearance = Appearance::as_ref(app);
|
||||
let choice = self.choice(app);
|
||||
|
||||
let slide = layout::static_left(
|
||||
|| self.render_content(appearance, choice, app),
|
||||
|| self.render_visual(),
|
||||
);
|
||||
|
||||
// Overlay the fallback bar at the bottom (rather than adding it to the
|
||||
// column) so the slide layout stays stable whether or not it's shown.
|
||||
let show_bar = self.show_auth_prompt_bar
|
||||
&& !matches!(
|
||||
self.onboarding_state.as_ref(app).auth_state(),
|
||||
OnboardingAuthState::PayingUser,
|
||||
);
|
||||
if !show_bar {
|
||||
return slide;
|
||||
}
|
||||
|
||||
let mut stack = Stack::new();
|
||||
stack.add_child(slide);
|
||||
stack.add_child(
|
||||
Align::new(self.render_auth_prompt_bar(appearance))
|
||||
.bottom_center()
|
||||
.finish(),
|
||||
);
|
||||
stack.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl AiAccessSlide {
|
||||
fn select_choice(&mut self, choice: AiAccessChoice, ctx: &mut ViewContext<Self>) {
|
||||
self.onboarding_state.update(ctx, |model, ctx| {
|
||||
model.set_ai_access_choice(choice, ctx);
|
||||
});
|
||||
ctx.notify();
|
||||
}
|
||||
|
||||
fn next(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
self.onboarding_state.update(ctx, |model, ctx| {
|
||||
model.next(ctx);
|
||||
});
|
||||
}
|
||||
|
||||
/// Primary "Next" action. On the subscription path this advances when the
|
||||
/// user already has a plan, otherwise it launches checkout in the browser
|
||||
/// (the slide auto-advances once billing flips to a paying plan). The "Set
|
||||
/// up later" path always advances.
|
||||
fn advance_or_upgrade(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
match self.choice(ctx) {
|
||||
AiAccessChoice::Subscription => {
|
||||
if matches!(
|
||||
self.onboarding_state.as_ref(ctx).auth_state(),
|
||||
OnboardingAuthState::PayingUser
|
||||
) {
|
||||
self.next(ctx);
|
||||
} else {
|
||||
// Surface the manual-fallback bar in case the browser
|
||||
// doesn't launch; it's hidden again once billing flips to
|
||||
// PayingUser.
|
||||
self.show_auth_prompt_bar = true;
|
||||
self.onboarding_state.update(ctx, |model, ctx| {
|
||||
model.request_upgrade(ctx);
|
||||
});
|
||||
ctx.notify();
|
||||
}
|
||||
}
|
||||
AiAccessChoice::SetUpLater => self.next(ctx),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl OnboardingSlide for AiAccessSlide {
|
||||
fn on_up(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
self.select_choice(AiAccessChoice::Subscription, ctx);
|
||||
}
|
||||
|
||||
fn on_down(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
self.select_choice(AiAccessChoice::SetUpLater, ctx);
|
||||
}
|
||||
|
||||
fn on_enter(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
self.advance_or_upgrade(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
impl TypedActionView for AiAccessSlide {
|
||||
type Action = AiAccessSlideAction;
|
||||
|
||||
fn handle_action(&mut self, action: &Self::Action, ctx: &mut ViewContext<Self>) {
|
||||
match action {
|
||||
AiAccessSlideAction::SelectSubscription => {
|
||||
self.select_choice(AiAccessChoice::Subscription, ctx);
|
||||
}
|
||||
AiAccessSlideAction::SelectSetUpLater => {
|
||||
self.select_choice(AiAccessChoice::SetUpLater, ctx);
|
||||
}
|
||||
AiAccessSlideAction::CopyUpgradeUrlClicked => {
|
||||
ctx.emit(AiAccessSlideEvent::CopyUpgradeUrlRequested);
|
||||
}
|
||||
AiAccessSlideAction::PasteAuthTokenFromClipboardClicked => {
|
||||
ctx.emit(AiAccessSlideEvent::PasteAuthTokenFromClipboardRequested);
|
||||
}
|
||||
AiAccessSlideAction::BackClicked => {
|
||||
self.onboarding_state.update(ctx, |model, ctx| {
|
||||
model.back(ctx);
|
||||
});
|
||||
}
|
||||
AiAccessSlideAction::NextClicked => {
|
||||
self.advance_or_upgrade(ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,520 @@
|
||||
use ui_components::{button, Component as _, Options as _};
|
||||
use galaxy_core::ui::appearance::Appearance;
|
||||
use galaxy_core::ui::theme::color::internal_colors;
|
||||
use galaxy_core::ui::theme::Fill;
|
||||
use galaxy_core::ui::Icon;
|
||||
use galaxyui_core::elements::{
|
||||
Border, ClippedScrollStateHandle, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment,
|
||||
Flex, FormattedTextElement, Hoverable, MainAxisAlignment, MainAxisSize, MouseStateHandle,
|
||||
ParentElement, Radius,
|
||||
};
|
||||
use galaxyui_core::fonts::Weight;
|
||||
use galaxyui_core::keymap::Keystroke;
|
||||
use galaxyui_core::platform::Cursor;
|
||||
use galaxyui_core::prelude::Align;
|
||||
use galaxyui_core::text_layout::TextAlignment;
|
||||
use galaxyui_core::ui_components::components::{UiComponent as _, UiComponentStyles};
|
||||
use galaxyui_core::{
|
||||
AppContext, Element, Entity, ModelHandle, SingletonEntity as _, TypedActionView, View,
|
||||
ViewContext,
|
||||
};
|
||||
|
||||
use super::OnboardingSlide;
|
||||
use crate::model::{AiSetupChoice, OnboardingStateModel};
|
||||
use crate::slides::{bottom_nav, layout, slide_content};
|
||||
|
||||
/// Checklist shown on the "Use Warp agent" card.
|
||||
const WARP_AGENT_FEATURES: &[&str] = &[
|
||||
"Best harness for terminal tasks and agentic coding",
|
||||
"Frontier models from OpenAI, Anthropic, and Google",
|
||||
"Model routing across frontier and open-weight models",
|
||||
"Multi-agent orchestration",
|
||||
];
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum AiSetupSlideAction {
|
||||
SelectWarpAgent,
|
||||
SelectThirdParty,
|
||||
BackClicked,
|
||||
NextClicked,
|
||||
}
|
||||
|
||||
/// The "Choose your AI setup" slide (DES-816 V3), shown on the AI-first path for
|
||||
/// users enrolled in the FREE_AI_REMOVAL experiment arm. Forks between the Warp
|
||||
/// agent (paid-plan path) and third-party agents (works on Free).
|
||||
pub struct AiSetupSlide {
|
||||
onboarding_state: ModelHandle<OnboardingStateModel>,
|
||||
warp_agent_mouse_state: MouseStateHandle,
|
||||
third_party_mouse_state: MouseStateHandle,
|
||||
back_button: button::Button,
|
||||
next_button: button::Button,
|
||||
scroll_state: ClippedScrollStateHandle,
|
||||
}
|
||||
|
||||
impl AiSetupSlide {
|
||||
pub(crate) fn new(onboarding_state: ModelHandle<OnboardingStateModel>) -> Self {
|
||||
Self {
|
||||
onboarding_state,
|
||||
warp_agent_mouse_state: MouseStateHandle::default(),
|
||||
third_party_mouse_state: MouseStateHandle::default(),
|
||||
back_button: button::Button::default(),
|
||||
next_button: button::Button::default(),
|
||||
scroll_state: ClippedScrollStateHandle::new(),
|
||||
}
|
||||
}
|
||||
|
||||
// The final DES-816 visual exports have not landed yet, so the right panel
|
||||
// reuses existing bundled assets that match each choice: the agent
|
||||
// experience for "Use Warp agent" and the CLI-agent toolbar for
|
||||
// "Use third party agents".
|
||||
pub(crate) const VISUAL_IMAGE_PATHS: &'static [&'static str] = &[
|
||||
"async/png/onboarding/welcome_agent.png",
|
||||
"async/png/onboarding/thirdparty_toolbar_enabled_vertical.png",
|
||||
];
|
||||
|
||||
fn choice(&self, app: &AppContext) -> AiSetupChoice {
|
||||
self.onboarding_state.as_ref(app).ai_setup_choice()
|
||||
}
|
||||
|
||||
fn render_content(
|
||||
&self,
|
||||
appearance: &Appearance,
|
||||
choice: AiSetupChoice,
|
||||
app: &AppContext,
|
||||
) -> Box<dyn Element> {
|
||||
let bottom_nav = Align::new(self.render_bottom_nav(appearance, app)).finish();
|
||||
|
||||
slide_content::onboarding_slide_content(
|
||||
vec![
|
||||
Align::new(self.render_header(appearance)).left().finish(),
|
||||
Align::new(self.render_options(appearance, choice)).finish(),
|
||||
],
|
||||
bottom_nav,
|
||||
self.scroll_state.clone(),
|
||||
appearance,
|
||||
)
|
||||
}
|
||||
|
||||
fn render_header(&self, appearance: &Appearance) -> Box<dyn Element> {
|
||||
let theme = appearance.theme();
|
||||
|
||||
let logo_fill = internal_colors::fg_overlay_4(theme);
|
||||
let logo = ConstrainedBox::new(Icon::WarpLogoLight.to_warpui_icon(logo_fill).finish())
|
||||
.with_width(64.)
|
||||
.with_height(64.)
|
||||
.finish();
|
||||
|
||||
let title = appearance
|
||||
.ui_builder()
|
||||
.paragraph("Choose your AI setup")
|
||||
.with_style(UiComponentStyles {
|
||||
font_size: Some(36.),
|
||||
font_weight: Some(Weight::Medium),
|
||||
..Default::default()
|
||||
})
|
||||
.build()
|
||||
.finish();
|
||||
|
||||
let subtitle = FormattedTextElement::from_str(
|
||||
"Choose if you'd like to use Warp Agent or third party agents.",
|
||||
appearance.ui_font_family(),
|
||||
16.,
|
||||
)
|
||||
.with_color(internal_colors::text_sub(
|
||||
theme,
|
||||
theme.background().into_solid(),
|
||||
))
|
||||
.with_weight(Weight::Normal)
|
||||
.with_alignment(TextAlignment::Left)
|
||||
.with_line_height_ratio(1.0)
|
||||
.finish();
|
||||
|
||||
Flex::column()
|
||||
.with_main_axis_size(MainAxisSize::Min)
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Start)
|
||||
// Offset icon built in padding to left align icon with title.
|
||||
.with_child(Container::new(logo).with_margin_left(-7.).finish())
|
||||
.with_child(Container::new(title).with_margin_top(11.).finish())
|
||||
.with_child(Container::new(subtitle).with_margin_top(16.).finish())
|
||||
.finish()
|
||||
}
|
||||
|
||||
fn render_options(&self, appearance: &Appearance, choice: AiSetupChoice) -> Box<dyn Element> {
|
||||
let warp_agent_card = self.render_warp_agent_card(
|
||||
appearance,
|
||||
matches!(choice, AiSetupChoice::WarpAgent),
|
||||
self.warp_agent_mouse_state.clone(),
|
||||
);
|
||||
|
||||
let third_party_card = self.render_third_party_card(
|
||||
appearance,
|
||||
matches!(choice, AiSetupChoice::ThirdParty),
|
||||
self.third_party_mouse_state.clone(),
|
||||
);
|
||||
|
||||
Container::new(
|
||||
Flex::column()
|
||||
.with_main_axis_size(MainAxisSize::Min)
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Stretch)
|
||||
.with_child(
|
||||
Container::new(warp_agent_card)
|
||||
.with_margin_bottom(12.)
|
||||
.finish(),
|
||||
)
|
||||
.with_child(third_party_card)
|
||||
.finish(),
|
||||
)
|
||||
.with_margin_top(38.)
|
||||
.finish()
|
||||
}
|
||||
|
||||
/// Shared chrome for an AI-setup option card. Applies the selected/unselected
|
||||
/// background + border + rounded corners, wires up hover/click, and emits the
|
||||
/// provided select action.
|
||||
fn render_card_chrome(
|
||||
appearance: &Appearance,
|
||||
is_selected: bool,
|
||||
mouse_state: MouseStateHandle,
|
||||
select_action: AiSetupSlideAction,
|
||||
content: Box<dyn Element>,
|
||||
) -> Box<dyn Element> {
|
||||
const RADIUS: f32 = 8.;
|
||||
|
||||
let theme = appearance.theme();
|
||||
let background = if is_selected {
|
||||
Some(internal_colors::accent_overlay_1(theme))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let border_color = if is_selected {
|
||||
theme.accent()
|
||||
} else {
|
||||
Fill::Solid(internal_colors::neutral_4(theme))
|
||||
};
|
||||
|
||||
Hoverable::new(mouse_state, move |_| {
|
||||
let mut container = Container::new(content)
|
||||
.with_uniform_padding(24.)
|
||||
.with_corner_radius(CornerRadius::with_all(Radius::Pixels(RADIUS)))
|
||||
.with_border(Border::all(1.).with_border_fill(border_color));
|
||||
if let Some(bg) = background {
|
||||
container = container.with_background(bg);
|
||||
}
|
||||
container.finish()
|
||||
})
|
||||
.with_cursor(Cursor::PointingHand)
|
||||
.on_click(move |ctx, _, _| {
|
||||
ctx.dispatch_typed_action(select_action.clone());
|
||||
})
|
||||
.finish()
|
||||
}
|
||||
|
||||
fn render_warp_agent_card(
|
||||
&self,
|
||||
appearance: &Appearance,
|
||||
is_selected: bool,
|
||||
mouse_state: MouseStateHandle,
|
||||
) -> Box<dyn Element> {
|
||||
let theme = appearance.theme();
|
||||
let bg_solid = theme.background().into_solid();
|
||||
let label_color = if is_selected {
|
||||
internal_colors::text_main(theme, bg_solid)
|
||||
} else {
|
||||
internal_colors::text_sub(theme, bg_solid)
|
||||
};
|
||||
let description_color = internal_colors::text_sub(theme, bg_solid);
|
||||
|
||||
let header_row = {
|
||||
let label = appearance
|
||||
.ui_builder()
|
||||
.paragraph("Use Warp Agent")
|
||||
.with_style(UiComponentStyles {
|
||||
font_size: Some(16.),
|
||||
font_weight: Some(Weight::Semibold),
|
||||
font_color: Some(label_color),
|
||||
..Default::default()
|
||||
})
|
||||
.build()
|
||||
.finish();
|
||||
|
||||
let badge = {
|
||||
let green = theme.ansi_fg_green();
|
||||
let badge_text = appearance
|
||||
.ui_builder()
|
||||
.paragraph("Access more models")
|
||||
.with_style(UiComponentStyles {
|
||||
font_size: Some(12.),
|
||||
font_weight: Some(Weight::Normal),
|
||||
font_color: Some(green),
|
||||
..Default::default()
|
||||
})
|
||||
.build()
|
||||
.finish();
|
||||
Container::new(badge_text)
|
||||
.with_corner_radius(CornerRadius::with_all(Radius::Pixels(11.)))
|
||||
.with_border(Border::all(1.).with_border_fill(Fill::Solid(green)))
|
||||
.with_horizontal_padding(8.)
|
||||
.with_vertical_padding(3.)
|
||||
.finish()
|
||||
};
|
||||
|
||||
Flex::row()
|
||||
.with_main_axis_size(MainAxisSize::Max)
|
||||
.with_main_axis_alignment(MainAxisAlignment::SpaceBetween)
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Center)
|
||||
.with_child(label)
|
||||
.with_child(badge)
|
||||
.finish()
|
||||
};
|
||||
|
||||
let description = FormattedTextElement::from_str(
|
||||
"State of the art agent harness deeply integrated into the terminal.",
|
||||
appearance.ui_font_family(),
|
||||
14.,
|
||||
)
|
||||
.with_color(description_color)
|
||||
.with_weight(Weight::Normal)
|
||||
.with_alignment(TextAlignment::Left)
|
||||
.with_line_height_ratio(1.2)
|
||||
.finish();
|
||||
|
||||
let checklist = {
|
||||
// When the card is selected, use the theme's green to match the
|
||||
// "Blended ANSI/green_fg" token in the design.
|
||||
let check_fill = if is_selected {
|
||||
Fill::Solid(theme.ansi_fg_green())
|
||||
} else {
|
||||
Fill::Solid(label_color)
|
||||
};
|
||||
let mut col = Flex::column()
|
||||
.with_main_axis_size(MainAxisSize::Min)
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Start);
|
||||
for &item in WARP_AGENT_FEATURES {
|
||||
let icon_el = ConstrainedBox::new(Icon::Check.to_warpui_icon(check_fill).finish())
|
||||
.with_width(16.)
|
||||
.with_height(16.)
|
||||
.finish();
|
||||
let text_el = appearance
|
||||
.ui_builder()
|
||||
.paragraph(item.to_string())
|
||||
.with_style(UiComponentStyles {
|
||||
font_size: Some(14.),
|
||||
font_weight: Some(Weight::Normal),
|
||||
font_color: Some(label_color),
|
||||
..Default::default()
|
||||
})
|
||||
.build()
|
||||
.finish();
|
||||
let row = Flex::row()
|
||||
.with_main_axis_size(MainAxisSize::Min)
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Center)
|
||||
.with_child(icon_el)
|
||||
.with_child(Container::new(text_el).with_margin_left(8.).finish())
|
||||
.finish();
|
||||
col = col.with_child(
|
||||
Container::new(row)
|
||||
.with_padding_top(4.)
|
||||
.with_padding_bottom(4.)
|
||||
.finish(),
|
||||
);
|
||||
}
|
||||
col.finish()
|
||||
};
|
||||
|
||||
let content = Flex::column()
|
||||
.with_main_axis_size(MainAxisSize::Min)
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Stretch)
|
||||
.with_child(header_row)
|
||||
.with_child(Container::new(description).with_margin_top(12.).finish())
|
||||
.with_child(Container::new(checklist).with_margin_top(12.).finish())
|
||||
.finish();
|
||||
|
||||
Self::render_card_chrome(
|
||||
appearance,
|
||||
is_selected,
|
||||
mouse_state,
|
||||
AiSetupSlideAction::SelectWarpAgent,
|
||||
content,
|
||||
)
|
||||
}
|
||||
|
||||
fn render_third_party_card(
|
||||
&self,
|
||||
appearance: &Appearance,
|
||||
is_selected: bool,
|
||||
mouse_state: MouseStateHandle,
|
||||
) -> Box<dyn Element> {
|
||||
let theme = appearance.theme();
|
||||
let bg_solid = theme.background().into_solid();
|
||||
let text_color = if is_selected {
|
||||
internal_colors::text_main(theme, bg_solid)
|
||||
} else {
|
||||
internal_colors::text_sub(theme, bg_solid)
|
||||
};
|
||||
|
||||
let label = appearance
|
||||
.ui_builder()
|
||||
.paragraph("Use third party agents")
|
||||
.with_style(UiComponentStyles {
|
||||
font_size: Some(16.),
|
||||
font_weight: Some(Weight::Semibold),
|
||||
font_color: Some(text_color),
|
||||
..Default::default()
|
||||
})
|
||||
.build()
|
||||
.finish();
|
||||
|
||||
let description = FormattedTextElement::from_str(
|
||||
"Use agents like Claude Code, Codex, and Gemini.",
|
||||
appearance.ui_font_family(),
|
||||
14.,
|
||||
)
|
||||
.with_color(text_color)
|
||||
.with_weight(Weight::Normal)
|
||||
.with_alignment(TextAlignment::Left)
|
||||
.with_line_height_ratio(1.2)
|
||||
.finish();
|
||||
|
||||
let content = Flex::column()
|
||||
.with_main_axis_size(MainAxisSize::Min)
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Stretch)
|
||||
.with_child(label)
|
||||
.with_child(Container::new(description).with_margin_top(12.).finish())
|
||||
.finish();
|
||||
|
||||
Self::render_card_chrome(
|
||||
appearance,
|
||||
is_selected,
|
||||
mouse_state,
|
||||
AiSetupSlideAction::SelectThirdParty,
|
||||
content,
|
||||
)
|
||||
}
|
||||
|
||||
fn render_bottom_nav(&self, appearance: &Appearance, app: &AppContext) -> Box<dyn Element> {
|
||||
let back_button = self.back_button.render(
|
||||
appearance,
|
||||
button::Params {
|
||||
content: button::Content::Label("Back".into()),
|
||||
theme: &button::themes::Naked,
|
||||
options: button::Options {
|
||||
on_click: Some(Box::new(|ctx, _app, _pos| {
|
||||
ctx.dispatch_typed_action(AiSetupSlideAction::BackClicked);
|
||||
})),
|
||||
..button::Options::default(appearance)
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
let enter = Keystroke::parse("enter").unwrap_or_default();
|
||||
let next_button = self.next_button.render(
|
||||
appearance,
|
||||
button::Params {
|
||||
content: button::Content::Label("Next".into()),
|
||||
theme: &button::themes::Primary,
|
||||
options: button::Options {
|
||||
keystroke: Some(enter),
|
||||
on_click: Some(Box::new(|ctx, _app, _pos| {
|
||||
ctx.dispatch_typed_action(AiSetupSlideAction::NextClicked);
|
||||
})),
|
||||
..button::Options::default(appearance)
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
let (step_index, step_count) = self.onboarding_state.as_ref(app).progress();
|
||||
bottom_nav::onboarding_bottom_nav(
|
||||
appearance,
|
||||
step_index,
|
||||
step_count,
|
||||
Some(back_button),
|
||||
Some(next_button),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_visual(&self, choice: AiSetupChoice) -> Box<dyn Element> {
|
||||
match choice {
|
||||
AiSetupChoice::WarpAgent => layout::onboarding_right_panel_with_bg(
|
||||
Self::VISUAL_IMAGE_PATHS[0],
|
||||
layout::FOREGROUND_LAYOUT_DEFAULT,
|
||||
),
|
||||
AiSetupChoice::ThirdParty => layout::onboarding_right_panel_with_bg(
|
||||
Self::VISUAL_IMAGE_PATHS[1],
|
||||
layout::FOREGROUND_LAYOUT_THIRD_PARTY,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for AiSetupSlide {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl View for AiSetupSlide {
|
||||
fn ui_name() -> &'static str {
|
||||
"AiSetupSlide"
|
||||
}
|
||||
|
||||
fn render(&self, app: &AppContext) -> Box<dyn Element> {
|
||||
let appearance = Appearance::as_ref(app);
|
||||
let choice = self.choice(app);
|
||||
|
||||
// Background is rendered by the parent onboarding view (including background images).
|
||||
layout::static_left(
|
||||
|| self.render_content(appearance, choice, app),
|
||||
|| self.render_visual(choice),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl AiSetupSlide {
|
||||
fn select_choice(&mut self, choice: AiSetupChoice, ctx: &mut ViewContext<Self>) {
|
||||
self.onboarding_state.update(ctx, |model, ctx| {
|
||||
model.set_ai_setup_choice(choice, ctx);
|
||||
});
|
||||
ctx.notify();
|
||||
}
|
||||
|
||||
fn next(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
self.onboarding_state.update(ctx, |model, ctx| {
|
||||
model.next(ctx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl OnboardingSlide for AiSetupSlide {
|
||||
fn on_up(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
self.select_choice(AiSetupChoice::WarpAgent, ctx);
|
||||
}
|
||||
|
||||
fn on_down(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
self.select_choice(AiSetupChoice::ThirdParty, ctx);
|
||||
}
|
||||
|
||||
fn on_enter(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
self.next(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
impl TypedActionView for AiSetupSlide {
|
||||
type Action = AiSetupSlideAction;
|
||||
|
||||
fn handle_action(&mut self, action: &Self::Action, ctx: &mut ViewContext<Self>) {
|
||||
match action {
|
||||
AiSetupSlideAction::SelectWarpAgent => {
|
||||
self.select_choice(AiSetupChoice::WarpAgent, ctx);
|
||||
}
|
||||
AiSetupSlideAction::SelectThirdParty => {
|
||||
self.select_choice(AiSetupChoice::ThirdParty, ctx);
|
||||
}
|
||||
AiSetupSlideAction::BackClicked => {
|
||||
self.onboarding_state.update(ctx, |model, ctx| {
|
||||
model.back(ctx);
|
||||
});
|
||||
}
|
||||
AiSetupSlideAction::NextClicked => {
|
||||
self.next(ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
use crate::slides::progress_dots;
|
||||
use galaxy_core::ui::appearance::Appearance;
|
||||
use galaxyui::{
|
||||
elements::{
|
||||
Align, Container, CrossAxisAlignment, Empty, Flex, MainAxisSize, ParentElement, Shrinkable,
|
||||
},
|
||||
Element,
|
||||
use galaxyui_core::elements::{
|
||||
Align, CrossAxisAlignment, Empty, Flex, MainAxisSize, ParentElement, Shrinkable,
|
||||
};
|
||||
use galaxyui_core::Element;
|
||||
|
||||
use crate::slides::progress_dots;
|
||||
|
||||
pub fn onboarding_bottom_nav(
|
||||
appearance: &Appearance,
|
||||
@@ -19,19 +18,16 @@ pub fn onboarding_bottom_nav(
|
||||
let back_button = back_button.unwrap_or_else(|| Empty::new().finish());
|
||||
let next_button = next_button.unwrap_or_else(|| Empty::new().finish());
|
||||
|
||||
// Use equal-size flex slots on the left and right so the dots remain centered regardless of the
|
||||
// button widths.
|
||||
// Equal-weight side slots push Back to the far left and Next to the far
|
||||
// right, leaving the natural-width dots centered between them on one row.
|
||||
let left = Shrinkable::new(1., Align::new(back_button).left().finish()).finish();
|
||||
let right = Shrinkable::new(1., Align::new(next_button).right().finish()).finish();
|
||||
|
||||
Container::new(
|
||||
Flex::row()
|
||||
.with_main_axis_size(MainAxisSize::Max)
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Center)
|
||||
.with_child(left)
|
||||
.with_child(dots)
|
||||
.with_child(right)
|
||||
.finish(),
|
||||
)
|
||||
.finish()
|
||||
Flex::row()
|
||||
.with_main_axis_size(MainAxisSize::Max)
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Center)
|
||||
.with_child(left)
|
||||
.with_child(dots)
|
||||
.with_child(right)
|
||||
.finish()
|
||||
}
|
||||
|
||||
@@ -1,25 +1,27 @@
|
||||
use ui_components::{button, Component as _, Options as _};
|
||||
use galaxy_core::features::FeatureFlag;
|
||||
use galaxy_core::ui::appearance::Appearance;
|
||||
use galaxy_core::ui::theme::color::internal_colors;
|
||||
use galaxyui_core::elements::{
|
||||
ClippedScrollStateHandle, Container, CrossAxisAlignment, Flex, FormattedTextElement,
|
||||
MainAxisSize, MouseStateHandle, ParentElement,
|
||||
};
|
||||
use galaxyui_core::fonts::Weight;
|
||||
use galaxyui_core::keymap::Keystroke;
|
||||
use galaxyui_core::prelude::Align;
|
||||
use galaxyui_core::text_layout::TextAlignment;
|
||||
use galaxyui_core::ui_components::components::{UiComponent as _, UiComponentStyles};
|
||||
use galaxyui_core::{
|
||||
AppContext, Element, Entity, ModelHandle, SingletonEntity as _, TypedActionView, View,
|
||||
ViewContext,
|
||||
};
|
||||
|
||||
use super::toggle_card::{render_toggle_card, ChipSpec, ToggleCardSpec};
|
||||
use super::OnboardingSlide;
|
||||
use crate::model::{OnboardingStateEvent, OnboardingStateModel, UICustomizationSettings};
|
||||
use crate::slides::{bottom_nav, layout, slide_content};
|
||||
use crate::visuals::{intention_terminal_visual, intention_visual};
|
||||
use crate::OnboardingIntention;
|
||||
use galaxy_core::features::FeatureFlag;
|
||||
use galaxy_core::ui::{appearance::Appearance, theme::color::internal_colors};
|
||||
use galaxyui::prelude::Align;
|
||||
use galaxyui::{
|
||||
elements::{
|
||||
ClippedScrollStateHandle, Container, CrossAxisAlignment, Flex, FormattedTextElement,
|
||||
MainAxisSize, MouseStateHandle, ParentElement,
|
||||
},
|
||||
fonts::Weight,
|
||||
keymap::Keystroke,
|
||||
text_layout::TextAlignment,
|
||||
ui_components::components::{UiComponent as _, UiComponentStyles},
|
||||
AppContext, Element, Entity, ModelHandle, SingletonEntity as _, TypedActionView, View,
|
||||
ViewContext,
|
||||
};
|
||||
use ui_components::{button, Component as _, Options as _};
|
||||
|
||||
/// Which setting card is currently selected (expanded).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
@@ -127,8 +129,9 @@ impl CustomizeUISlide {
|
||||
appearance: &Appearance,
|
||||
intention: OnboardingIntention,
|
||||
ui: &UICustomizationSettings,
|
||||
app: &AppContext,
|
||||
) -> Box<dyn Element> {
|
||||
let bottom_nav = Align::new(self.render_bottom_nav(appearance, intention)).finish();
|
||||
let bottom_nav = Align::new(self.render_bottom_nav(appearance, app)).finish();
|
||||
|
||||
slide_content::onboarding_slide_content(
|
||||
vec![
|
||||
@@ -255,6 +258,24 @@ impl CustomizeUISlide {
|
||||
let mut chips = vec![];
|
||||
|
||||
if ui.tools_panel_enabled(&intention) {
|
||||
chips.push(ChipSpec {
|
||||
label: "File explorer",
|
||||
is_enabled: ui.show_project_explorer,
|
||||
mouse_state: self.chip_file_explorer_mouse.clone(),
|
||||
on_click: Box::new(|ctx, _, _| {
|
||||
ctx.dispatch_typed_action(CustomizeSlideAction::ToggleToolsSubSetting {
|
||||
setting: ToolsPanelSubSetting::ProjectExplorer,
|
||||
});
|
||||
}),
|
||||
on_hover: Some(Box::new(|is_hovered, ctx, _, _| {
|
||||
if is_hovered {
|
||||
ctx.dispatch_typed_action(CustomizeSlideAction::HoverToolsChip {
|
||||
setting: ToolsPanelSubSetting::ProjectExplorer,
|
||||
});
|
||||
}
|
||||
})),
|
||||
});
|
||||
|
||||
// Conversation history chip is only shown for the agent intention.
|
||||
if is_agent {
|
||||
chips.push(ChipSpec {
|
||||
@@ -276,24 +297,6 @@ impl CustomizeUISlide {
|
||||
});
|
||||
}
|
||||
|
||||
chips.push(ChipSpec {
|
||||
label: "File explorer",
|
||||
is_enabled: ui.show_project_explorer,
|
||||
mouse_state: self.chip_file_explorer_mouse.clone(),
|
||||
on_click: Box::new(|ctx, _, _| {
|
||||
ctx.dispatch_typed_action(CustomizeSlideAction::ToggleToolsSubSetting {
|
||||
setting: ToolsPanelSubSetting::ProjectExplorer,
|
||||
});
|
||||
}),
|
||||
on_hover: Some(Box::new(|is_hovered, ctx, _, _| {
|
||||
if is_hovered {
|
||||
ctx.dispatch_typed_action(CustomizeSlideAction::HoverToolsChip {
|
||||
setting: ToolsPanelSubSetting::ProjectExplorer,
|
||||
});
|
||||
}
|
||||
})),
|
||||
});
|
||||
|
||||
chips.push(ChipSpec {
|
||||
label: "Global file search",
|
||||
is_enabled: ui.show_global_search,
|
||||
@@ -402,11 +405,7 @@ impl CustomizeUISlide {
|
||||
|
||||
// --- Bottom nav ---
|
||||
|
||||
fn render_bottom_nav(
|
||||
&self,
|
||||
appearance: &Appearance,
|
||||
intention: OnboardingIntention,
|
||||
) -> Box<dyn Element> {
|
||||
fn render_bottom_nav(&self, appearance: &Appearance, app: &AppContext) -> Box<dyn Element> {
|
||||
let back_button = self.back_button.render(
|
||||
appearance,
|
||||
button::Params {
|
||||
@@ -437,8 +436,7 @@ impl CustomizeUISlide {
|
||||
},
|
||||
);
|
||||
|
||||
let is_terminal = matches!(intention, OnboardingIntention::Terminal);
|
||||
let (step_index, step_count) = if is_terminal { (1, 4) } else { (1, 5) };
|
||||
let (step_index, step_count) = self.onboarding_state.as_ref(app).progress();
|
||||
bottom_nav::onboarding_bottom_nav(
|
||||
appearance,
|
||||
step_index,
|
||||
@@ -537,12 +535,8 @@ impl CustomizeUISlide {
|
||||
"async/png/onboarding/terminal_intention/terminal_customize_horizontal_tabs.png"
|
||||
}
|
||||
} else {
|
||||
// Default chip: conversation for agent, file explorer for terminal.
|
||||
let default_chip = if is_agent {
|
||||
ToolsPanelSubSetting::ConversationHistory
|
||||
} else {
|
||||
ToolsPanelSubSetting::ProjectExplorer
|
||||
};
|
||||
// Default chip: file explorer for both intents (matches the new tools panel order).
|
||||
let default_chip = ToolsPanelSubSetting::ProjectExplorer;
|
||||
let chip = hovered_chip.unwrap_or(default_chip);
|
||||
if is_agent {
|
||||
match (chip, vertical) {
|
||||
@@ -644,7 +638,7 @@ impl View for CustomizeUISlide {
|
||||
let ui = self.model_ui_customization(app);
|
||||
|
||||
layout::static_left(
|
||||
|| self.render_content(appearance, intention, &ui),
|
||||
|| self.render_content(appearance, intention, &ui, app),
|
||||
|| self.render_visual(appearance, intention, &ui),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,27 +1,30 @@
|
||||
use super::OnboardingSlide;
|
||||
use crate::model::OnboardingStateModel;
|
||||
use crate::slides::{bottom_nav, layout, slide_content};
|
||||
use crate::visuals::{intention_terminal_visual, intention_visual};
|
||||
use crate::{OnboardingIntention, AI_FEATURES};
|
||||
use ui_components::{button, Component as _, Options as _};
|
||||
use galaxy_core::features::FeatureFlag;
|
||||
use galaxy_core::ui::appearance::Appearance;
|
||||
use galaxy_core::ui::theme::color::internal_colors;
|
||||
use galaxy_core::ui::theme::Fill;
|
||||
use galaxy_core::ui::{appearance::Appearance, theme::color::internal_colors, Icon};
|
||||
use galaxyui::prelude::Align;
|
||||
use galaxyui::{
|
||||
elements::{
|
||||
Border, ClippedScrollStateHandle, ConstrainedBox, Container, CornerRadius,
|
||||
CrossAxisAlignment, Flex, FormattedTextElement, Hoverable, MainAxisAlignment, MainAxisSize,
|
||||
MouseStateHandle, ParentElement, Radius,
|
||||
},
|
||||
fonts::Weight,
|
||||
keymap::Keystroke,
|
||||
platform::Cursor,
|
||||
text_layout::TextAlignment,
|
||||
ui_components::components::{UiComponent as _, UiComponentStyles},
|
||||
use galaxy_core::ui::Icon;
|
||||
use galaxyui_core::elements::{
|
||||
Border, ClippedScrollStateHandle, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment,
|
||||
Flex, FormattedTextElement, Hoverable, MainAxisAlignment, MainAxisSize, MouseStateHandle,
|
||||
ParentElement, Radius,
|
||||
};
|
||||
use galaxyui_core::fonts::Weight;
|
||||
use galaxyui_core::keymap::Keystroke;
|
||||
use galaxyui_core::platform::Cursor;
|
||||
use galaxyui_core::prelude::Align;
|
||||
use galaxyui_core::text_layout::TextAlignment;
|
||||
use galaxyui_core::ui_components::components::{UiComponent as _, UiComponentStyles};
|
||||
use galaxyui_core::{
|
||||
AppContext, Element, Entity, ModelHandle, SingletonEntity as _, TypedActionView, View,
|
||||
ViewContext,
|
||||
};
|
||||
use ui_components::{button, Component as _, Options as _};
|
||||
|
||||
use super::OnboardingSlide;
|
||||
use crate::model::{NoAiConfirmationSource, OnboardingStateModel};
|
||||
use crate::slides::{bottom_nav, layout, slide_content};
|
||||
use crate::visuals::{intention_terminal_visual, intention_visual};
|
||||
use crate::{OnboardingIntention, AI_FEATURES};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum IntentionSlideAction {
|
||||
@@ -199,7 +202,7 @@ impl IntentionSlide {
|
||||
let header_row = {
|
||||
let label = appearance
|
||||
.ui_builder()
|
||||
.paragraph("Build faster with AI agents")
|
||||
.paragraph("Build faster with agents")
|
||||
.with_style(UiComponentStyles {
|
||||
font_size: Some(16.),
|
||||
font_weight: Some(Weight::Semibold),
|
||||
@@ -237,7 +240,7 @@ impl IntentionSlide {
|
||||
};
|
||||
|
||||
let description = FormattedTextElement::from_str(
|
||||
"An agent-first experience with best in class terminal support. Get terminal and agent driven development AI features like:",
|
||||
"Get AI features to accelerate terminal and agent-driven workflows:",
|
||||
appearance.ui_font_family(),
|
||||
14.,
|
||||
)
|
||||
@@ -521,8 +524,14 @@ impl IntentionSlide {
|
||||
fn next(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
self.onboarding_state.update(ctx, |model, ctx| {
|
||||
if FeatureFlag::OpenWarpNewSettingsModes.is_enabled() {
|
||||
// Always advance to Customize slide; both intentions continue the flow.
|
||||
model.next(ctx);
|
||||
match model.intention() {
|
||||
// "Just use the terminal" confirms leaving AI behind before advancing.
|
||||
OnboardingIntention::Terminal => {
|
||||
model.request_no_ai_confirmation(NoAiConfirmationSource::Intention, ctx);
|
||||
}
|
||||
// Agent intention routes to the next step (the AI-setup fork).
|
||||
OnboardingIntention::AgentDrivenDevelopment => model.next(ctx),
|
||||
}
|
||||
} else {
|
||||
match model.intention() {
|
||||
OnboardingIntention::Terminal => {
|
||||
|
||||
@@ -1,25 +1,29 @@
|
||||
use crate::model::OnboardingStateModel;
|
||||
use crate::OnboardingEvent;
|
||||
|
||||
use super::OnboardingSlide;
|
||||
use galaxy_core::send_telemetry_from_ctx;
|
||||
use galaxy_core::ui::{appearance::Appearance, theme::color::internal_colors, Icon};
|
||||
use galaxyui::{
|
||||
elements::{
|
||||
shimmering_text::{ShimmerConfig, ShimmeringTextElement, ShimmeringTextStateHandle},
|
||||
Align, ChildAnchor, ConstrainedBox, Container, CrossAxisAlignment, Flex,
|
||||
FormattedTextElement, MainAxisAlignment, MainAxisSize, MouseStateHandle, OffsetPositioning,
|
||||
ParentAnchor, ParentElement, ParentOffsetBounds, Stack,
|
||||
},
|
||||
keymap::Keystroke,
|
||||
text_layout::TextAlignment,
|
||||
ui_components::components::{UiComponent as _, UiComponentStyles},
|
||||
AppContext, Element, Entity, ModelHandle, SingletonEntity as _, TypedActionView, View,
|
||||
ViewContext,
|
||||
};
|
||||
use pathfinder_color::ColorU;
|
||||
use pathfinder_geometry::vector::vec2f;
|
||||
use ui_components::{button, Component as _, Options as _};
|
||||
use galaxy_core::send_telemetry_from_ctx;
|
||||
use galaxy_core::ui::appearance::Appearance;
|
||||
use galaxy_core::ui::theme::color::internal_colors;
|
||||
use galaxy_core::ui::Icon;
|
||||
use galaxyui_core::elements::shimmering_text::{
|
||||
ShimmerConfig, ShimmeringTextElement, ShimmeringTextStateHandle,
|
||||
};
|
||||
use galaxyui_core::elements::{
|
||||
Align, ChildAnchor, ConstrainedBox, Container, CrossAxisAlignment, Flex, FormattedTextElement,
|
||||
MainAxisAlignment, MainAxisSize, MouseStateHandle, OffsetPositioning, ParentAnchor,
|
||||
ParentElement, ParentOffsetBounds, Stack,
|
||||
};
|
||||
use galaxyui_core::keymap::Keystroke;
|
||||
use galaxyui_core::text_layout::TextAlignment;
|
||||
use galaxyui_core::ui_components::components::{UiComponent as _, UiComponentStyles};
|
||||
use galaxyui_core::{
|
||||
AppContext, Element, Entity, ModelHandle, SingletonEntity as _, TypedActionView, View,
|
||||
ViewContext,
|
||||
};
|
||||
|
||||
use super::OnboardingSlide;
|
||||
use crate::model::OnboardingStateModel;
|
||||
use crate::OnboardingEvent;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum IntroSlideEvent {
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
use galaxyui::{
|
||||
assets::asset_cache::AssetSource,
|
||||
elements::{
|
||||
Align, CacheOption, Clipped, ConstrainedBox, Container, CrossAxisAlignment, Empty,
|
||||
Expanded, Flex, Image, MainAxisSize, ParentElement, Point, Shrinkable,
|
||||
SizeConstraintCondition, SizeConstraintSwitch, Stack,
|
||||
},
|
||||
event::DispatchedEvent,
|
||||
use pathfinder_geometry::vector::{vec2f, Vector2F};
|
||||
use galaxyui_core::assets::asset_cache::AssetSource;
|
||||
use galaxyui_core::elements::{
|
||||
Align, CacheOption, Clipped, ConstrainedBox, Container, CrossAxisAlignment, Empty, Expanded,
|
||||
Flex, Image, MainAxisSize, ParentElement, Point, Shrinkable, SizeConstraintCondition,
|
||||
SizeConstraintSwitch, Stack,
|
||||
};
|
||||
use galaxyui_core::event::DispatchedEvent;
|
||||
use galaxyui_core::{
|
||||
AfterLayoutContext, AppContext, Element, EventContext, LayoutContext, PaintContext,
|
||||
SizeConstraint,
|
||||
};
|
||||
use pathfinder_geometry::vector::{vec2f, Vector2F};
|
||||
|
||||
// Onboarding images live under `app/assets/async/` so they are excluded from the WASM
|
||||
// binary (RustEmbed excludes `async/**` on wasm targets). They are still bundled normally
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
mod agent_slide;
|
||||
mod ai_access_slide;
|
||||
mod ai_setup_slide;
|
||||
mod bottom_nav;
|
||||
mod customize_slide;
|
||||
mod free_user_no_ai_slide;
|
||||
mod intention_slide;
|
||||
mod intro_slide;
|
||||
pub mod layout;
|
||||
@@ -14,12 +15,11 @@ mod third_party_slide;
|
||||
mod toggle_card;
|
||||
mod two_line_button;
|
||||
|
||||
pub use agent_slide::{
|
||||
AgentAutonomy, AgentDevelopmentSettings, AgentSlide, AgentSlideEvent, OnboardingModelInfo,
|
||||
};
|
||||
pub use agent_slide::{AgentAutonomy, AgentDevelopmentSettings, AgentSlide, OnboardingModelInfo};
|
||||
pub use ai_access_slide::{AiAccessSlide, AiAccessSlideEvent};
|
||||
pub use ai_setup_slide::AiSetupSlide;
|
||||
pub use bottom_nav::onboarding_bottom_nav;
|
||||
pub use customize_slide::CustomizeUISlide;
|
||||
pub use free_user_no_ai_slide::FreeUserNoAiSlide;
|
||||
pub use intention_slide::IntentionSlide;
|
||||
pub use intro_slide::{IntroSlide, IntroSlideEvent};
|
||||
pub use onboarding_slide::OnboardingSlide;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use galaxyui::{View, ViewContext};
|
||||
use galaxyui_core::{View, ViewContext};
|
||||
|
||||
pub trait OnboardingSlide: View {
|
||||
fn on_up(&mut self, _ctx: &mut ViewContext<Self>) {}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
use galaxy_core::ui::{appearance::Appearance, theme::color::internal_colors};
|
||||
use galaxyui::{
|
||||
elements::{
|
||||
ConstrainedBox, Container, CornerRadius, Empty, Flex, MainAxisSize, ParentElement, Radius,
|
||||
},
|
||||
Element,
|
||||
use galaxy_core::ui::appearance::Appearance;
|
||||
use galaxy_core::ui::theme::color::internal_colors;
|
||||
use galaxyui_core::elements::{
|
||||
ConstrainedBox, Container, CornerRadius, Empty, Flex, MainAxisSize, ParentElement, Radius,
|
||||
};
|
||||
use galaxyui_core::Element;
|
||||
|
||||
/// Render `n` dots with 4px radius and 8px spacing. `k` is the 0-based active dot index.
|
||||
pub(crate) fn progress_dots(n: usize, k: usize, appearance: &Appearance) -> Box<dyn Element> {
|
||||
|
||||
@@ -1,28 +1,29 @@
|
||||
use ui_components::{button, keyboard_shortcut, Component as _, Options as _};
|
||||
use galaxy_core::send_telemetry_from_ctx;
|
||||
use galaxy_core::ui::appearance::Appearance;
|
||||
use galaxy_core::ui::color::coloru_with_opacity;
|
||||
use galaxy_core::ui::theme::color::internal_colors;
|
||||
use galaxy_core::ui::Icon;
|
||||
use galaxyui_core::elements::{
|
||||
Align, ClippedScrollStateHandle, ConstrainedBox, Container, CrossAxisAlignment, Flex,
|
||||
MouseStateHandle, ParentElement, Shrinkable,
|
||||
};
|
||||
use galaxyui_core::fonts::Weight;
|
||||
use galaxyui_core::keymap::Keystroke;
|
||||
use galaxyui_core::platform::file_picker::{FilePickerConfiguration, FilePickerError};
|
||||
use galaxyui_core::prelude::{MainAxisAlignment, MainAxisSize, Vector2F};
|
||||
use galaxyui_core::ui_components::button::{ButtonVariant, TextAndIcon, TextAndIconAlignment};
|
||||
use galaxyui_core::ui_components::components::{UiComponent as _, UiComponentStyles};
|
||||
use galaxyui_core::{
|
||||
AppContext, Element, Entity, ModelHandle, SingletonEntity as _, TypedActionView, View,
|
||||
ViewContext,
|
||||
};
|
||||
|
||||
use super::OnboardingSlide;
|
||||
use crate::model::OnboardingStateModel;
|
||||
use crate::slides::{bottom_nav, layout, slide_content};
|
||||
use crate::telemetry::OnboardingEvent;
|
||||
use crate::visuals::project_visual;
|
||||
use galaxy_core::send_telemetry_from_ctx;
|
||||
use galaxy_core::ui::{
|
||||
appearance::Appearance, color::coloru_with_opacity, theme::color::internal_colors, Icon,
|
||||
};
|
||||
use galaxyui::prelude::{MainAxisAlignment, MainAxisSize, Vector2F};
|
||||
use galaxyui::ui_components::button::{ButtonVariant, TextAndIcon, TextAndIconAlignment};
|
||||
use galaxyui::{
|
||||
elements::{
|
||||
Align, ClippedScrollStateHandle, ConstrainedBox, Container, CrossAxisAlignment, Flex,
|
||||
MouseStateHandle, ParentElement, Shrinkable,
|
||||
},
|
||||
fonts::Weight,
|
||||
keymap::Keystroke,
|
||||
platform::file_picker::{FilePickerConfiguration, FilePickerError},
|
||||
ui_components::components::{UiComponent as _, UiComponentStyles},
|
||||
AppContext, Element, Entity, ModelHandle, SingletonEntity as _, TypedActionView, View,
|
||||
ViewContext,
|
||||
};
|
||||
use ui_components::{button, keyboard_shortcut, Component as _, Options as _};
|
||||
|
||||
use super::OnboardingSlide;
|
||||
|
||||
const LEFT_COLUMN_W: f32 = 428.;
|
||||
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
use galaxy_core::ui::appearance::Appearance;
|
||||
use galaxyui::{
|
||||
elements::{
|
||||
Align, ClippedScrollStateHandle, ClippedScrollable, Container, CrossAxisAlignment, Flex,
|
||||
MainAxisSize, ParentElement, ScrollbarWidth, Shrinkable,
|
||||
},
|
||||
Element,
|
||||
use galaxyui_core::elements::{
|
||||
Align, ClippedScrollStateHandle, ClippedScrollable, Container, CrossAxisAlignment, Flex,
|
||||
MainAxisSize, ParentElement, ScrollbarWidth, Shrinkable,
|
||||
};
|
||||
use galaxyui_core::Element;
|
||||
|
||||
pub fn onboarding_slide_content(
|
||||
children: Vec<Box<dyn Element>>,
|
||||
@@ -57,7 +55,7 @@ pub fn onboarding_slide_content(
|
||||
.with_child(Shrinkable::new(1., scrollable).finish())
|
||||
.with_child(
|
||||
Container::new(bottom_nav)
|
||||
.with_margin_top(24.)
|
||||
.with_margin_top(16.)
|
||||
.with_padding_right(PADDING)
|
||||
.finish(),
|
||||
)
|
||||
@@ -67,7 +65,7 @@ pub fn onboarding_slide_content(
|
||||
// inside the scrollable and bottom nav so the scrollbar stays at the edge.
|
||||
Container::new(outer)
|
||||
.with_padding_top(PADDING)
|
||||
.with_padding_bottom(PADDING)
|
||||
.with_padding_bottom(PADDING - 16.)
|
||||
.with_padding_left(PADDING)
|
||||
.finish()
|
||||
}
|
||||
|
||||
@@ -1,27 +1,30 @@
|
||||
use pathfinder_color::ColorU;
|
||||
use ui_components::{button, Component as _, Options as _};
|
||||
use galaxy_core::features::FeatureFlag;
|
||||
use galaxy_core::send_telemetry_from_ctx;
|
||||
use galaxy_core::ui::appearance::Appearance;
|
||||
use galaxy_core::ui::theme::color::internal_colors;
|
||||
use galaxy_core::ui::theme::WarpTheme;
|
||||
use galaxyui_core::elements::{
|
||||
Border, ClippedScrollStateHandle, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment,
|
||||
Empty, Flex, FormattedTextElement, Hoverable, MainAxisAlignment, MainAxisSize,
|
||||
MouseStateHandle, ParentElement, Radius, Text,
|
||||
};
|
||||
use galaxyui_core::fonts::{Properties, Weight};
|
||||
use galaxyui_core::keymap::Keystroke;
|
||||
use galaxyui_core::platform::Cursor;
|
||||
use galaxyui_core::text_layout::TextAlignment;
|
||||
use galaxyui_core::ui_components::components::{UiComponent, UiComponentStyles};
|
||||
use galaxyui_core::{
|
||||
AppContext, Element, Entity, ModelHandle, SingletonEntity, TypedActionView, View, ViewContext,
|
||||
};
|
||||
|
||||
use super::OnboardingSlide;
|
||||
use crate::model::{OnboardingStateEvent, OnboardingStateModel};
|
||||
use crate::slides::{bottom_nav, layout, slide_content};
|
||||
use crate::telemetry::OnboardingEvent;
|
||||
use crate::visuals::theme_picker_visual;
|
||||
use crate::OnboardingIntention;
|
||||
use galaxy_core::features::FeatureFlag;
|
||||
use galaxy_core::send_telemetry_from_ctx;
|
||||
use galaxy_core::ui::{appearance::Appearance, theme::color::internal_colors, theme::GalaxyTheme};
|
||||
use galaxyui::{
|
||||
elements::{
|
||||
Border, ClippedScrollStateHandle, ConstrainedBox, Container, CornerRadius,
|
||||
CrossAxisAlignment, Empty, Flex, FormattedTextElement, Hoverable, MainAxisAlignment,
|
||||
MainAxisSize, MouseStateHandle, ParentElement, Radius, Text,
|
||||
},
|
||||
fonts::{Properties, Weight},
|
||||
keymap::Keystroke,
|
||||
platform::Cursor,
|
||||
text_layout::TextAlignment,
|
||||
ui_components::components::{UiComponent, UiComponentStyles},
|
||||
AppContext, Element, Entity, ModelHandle, SingletonEntity, TypedActionView, View, ViewContext,
|
||||
};
|
||||
use pathfinder_color::ColorU;
|
||||
use ui_components::{button, Component as _, Options as _};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ThemePickerSlideEvent {
|
||||
@@ -134,12 +137,15 @@ impl ThemePickerSlide {
|
||||
app: &AppContext,
|
||||
) -> Box<dyn Element> {
|
||||
// The option "chrome" (background, borders, text) should be styled using the currently
|
||||
// selected theme.
|
||||
let selected_theme = self
|
||||
.theme_options
|
||||
.get(self.selected_theme_index)
|
||||
.map(|option| option.theme.clone())
|
||||
.unwrap_or_else(|| self.theme_options[0].theme.clone());
|
||||
// selected theme, if sync_with_os is not selected.
|
||||
let selected_theme = if self.sync_with_os {
|
||||
appearance.theme().clone()
|
||||
} else {
|
||||
self.theme_options
|
||||
.get(self.selected_theme_index)
|
||||
.map(|option| option.theme.clone())
|
||||
.unwrap_or_else(|| self.theme_options[0].theme.clone())
|
||||
};
|
||||
|
||||
let bottom_nav = self.render_bottom_nav(appearance, app);
|
||||
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
use ui_components::{button, Component as _, Options as _};
|
||||
use galaxy_core::ui::appearance::Appearance;
|
||||
use galaxy_core::ui::theme::color::internal_colors;
|
||||
use galaxyui_core::elements::{
|
||||
ClippedScrollStateHandle, Container, CrossAxisAlignment, Flex, FormattedTextElement,
|
||||
MainAxisSize, MouseStateHandle, ParentElement,
|
||||
};
|
||||
use galaxyui_core::fonts::Weight;
|
||||
use galaxyui_core::keymap::Keystroke;
|
||||
use galaxyui_core::prelude::Align;
|
||||
use galaxyui_core::text_layout::TextAlignment;
|
||||
use galaxyui_core::ui_components::components::{UiComponent as _, UiComponentStyles};
|
||||
use galaxyui_core::{
|
||||
AppContext, Element, Entity, ModelHandle, SingletonEntity as _, TypedActionView, View,
|
||||
ViewContext,
|
||||
};
|
||||
|
||||
use super::toggle_card::{render_toggle_card, ToggleCardSpec};
|
||||
use super::OnboardingSlide;
|
||||
use crate::model::{OnboardingStateEvent, OnboardingStateModel};
|
||||
use crate::slides::{bottom_nav, layout, slide_content};
|
||||
use crate::OnboardingIntention;
|
||||
|
||||
use galaxy_core::ui::appearance::Appearance;
|
||||
use galaxy_core::ui::theme::color::internal_colors;
|
||||
use galaxyui::prelude::Align;
|
||||
use galaxyui::{
|
||||
elements::{
|
||||
ClippedScrollStateHandle, Container, CrossAxisAlignment, Flex, FormattedTextElement,
|
||||
MainAxisSize, MouseStateHandle, ParentElement,
|
||||
},
|
||||
fonts::Weight,
|
||||
keymap::Keystroke,
|
||||
text_layout::TextAlignment,
|
||||
ui_components::components::{UiComponent as _, UiComponentStyles},
|
||||
AppContext, Element, Entity, ModelHandle, SingletonEntity as _, TypedActionView, View,
|
||||
ViewContext,
|
||||
};
|
||||
use ui_components::{button, Component as _, Options as _};
|
||||
|
||||
/// Which setting card is currently expanded.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum SettingCard {
|
||||
@@ -111,8 +111,9 @@ impl ThirdPartySlide {
|
||||
cli_toolbar_enabled: bool,
|
||||
show_agent_notifications: bool,
|
||||
intention: OnboardingIntention,
|
||||
app: &AppContext,
|
||||
) -> Box<dyn Element> {
|
||||
let bottom_nav = Align::new(self.render_bottom_nav(appearance, intention)).finish();
|
||||
let bottom_nav = Align::new(self.render_bottom_nav(appearance, app)).finish();
|
||||
|
||||
let mut sections = vec![
|
||||
self.render_header(appearance),
|
||||
@@ -263,11 +264,7 @@ impl ThirdPartySlide {
|
||||
.finish()
|
||||
}
|
||||
|
||||
fn render_bottom_nav(
|
||||
&self,
|
||||
appearance: &Appearance,
|
||||
intention: OnboardingIntention,
|
||||
) -> Box<dyn Element> {
|
||||
fn render_bottom_nav(&self, appearance: &Appearance, app: &AppContext) -> Box<dyn Element> {
|
||||
let back_button = self.back_button.render(
|
||||
appearance,
|
||||
button::Params {
|
||||
@@ -298,8 +295,7 @@ impl ThirdPartySlide {
|
||||
},
|
||||
);
|
||||
|
||||
let is_terminal = matches!(intention, OnboardingIntention::Terminal);
|
||||
let (step_index, step_count) = if is_terminal { (2, 4) } else { (3, 5) };
|
||||
let (step_index, step_count) = self.onboarding_state.as_ref(app).progress();
|
||||
bottom_nav::onboarding_bottom_nav(
|
||||
appearance,
|
||||
step_index,
|
||||
@@ -361,6 +357,7 @@ impl View for ThirdPartySlide {
|
||||
cli_toolbar_enabled,
|
||||
show_agent_notifications,
|
||||
intention,
|
||||
app,
|
||||
)
|
||||
},
|
||||
|| self.render_visual(cli_toolbar_enabled, show_agent_notifications, vertical),
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
use galaxy_core::ui::theme::Fill;
|
||||
use galaxy_core::ui::{appearance::Appearance, theme::color::internal_colors};
|
||||
use galaxyui::prelude::Align;
|
||||
use galaxyui::{
|
||||
elements::{
|
||||
Border, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment, Flex,
|
||||
FormattedTextElement, Hoverable, MainAxisAlignment, MainAxisSize, MouseStateHandle,
|
||||
ParentElement, Radius, Shrinkable, Text, Wrap,
|
||||
},
|
||||
fonts::Weight,
|
||||
platform::Cursor,
|
||||
presenter::EventContext,
|
||||
text_layout::TextAlignment,
|
||||
AppContext, Element,
|
||||
};
|
||||
use pathfinder_geometry::vector::Vector2F;
|
||||
use galaxy_core::ui::appearance::Appearance;
|
||||
use galaxy_core::ui::theme::color::internal_colors;
|
||||
use galaxy_core::ui::theme::Fill;
|
||||
use galaxyui_core::elements::{
|
||||
Border, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment, Flex,
|
||||
FormattedTextElement, Hoverable, MainAxisAlignment, MainAxisSize, MouseStateHandle,
|
||||
ParentElement, Radius, Shrinkable, Text, Wrap,
|
||||
};
|
||||
use galaxyui_core::fonts::Weight;
|
||||
use galaxyui_core::platform::Cursor;
|
||||
use galaxyui_core::prelude::Align;
|
||||
use galaxyui_core::presenter::EventContext;
|
||||
use galaxyui_core::text_layout::TextAlignment;
|
||||
use galaxyui_core::{AppContext, Element};
|
||||
|
||||
pub(super) type ClickCallback = Box<dyn FnMut(&mut EventContext, &AppContext, Vector2F) + 'static>;
|
||||
pub(super) type HoverCallback =
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
use super::agent_slide::AgentSlideAction;
|
||||
use galaxy_core::ui::{
|
||||
appearance::Appearance,
|
||||
icons::Icon,
|
||||
theme::{color::internal_colors, Fill},
|
||||
};
|
||||
use galaxyui::{
|
||||
elements::{
|
||||
Align, Border, ChildAnchor, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment,
|
||||
Flex, Hoverable, MainAxisSize, MouseStateHandle, OffsetPositioning, ParentAnchor,
|
||||
ParentElement, ParentOffsetBounds, Radius, Stack, Text,
|
||||
},
|
||||
fonts::{Properties, Weight},
|
||||
platform::Cursor,
|
||||
Element,
|
||||
};
|
||||
use pathfinder_geometry::vector::vec2f;
|
||||
use galaxy_core::ui::appearance::Appearance;
|
||||
use galaxy_core::ui::icons::Icon;
|
||||
use galaxy_core::ui::theme::color::internal_colors;
|
||||
use galaxy_core::ui::theme::Fill;
|
||||
use galaxyui_core::elements::{
|
||||
Align, Border, ChildAnchor, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment, Flex,
|
||||
Hoverable, MainAxisSize, MouseStateHandle, OffsetPositioning, ParentAnchor, ParentElement,
|
||||
ParentOffsetBounds, Radius, Stack, Text,
|
||||
};
|
||||
use galaxyui_core::fonts::{Properties, Weight};
|
||||
use galaxyui_core::platform::Cursor;
|
||||
use galaxyui_core::Element;
|
||||
|
||||
use super::agent_slide::AgentSlideAction;
|
||||
|
||||
pub(super) struct TwoLineButtonSpec {
|
||||
pub(super) is_selected: bool,
|
||||
|
||||
Reference in New Issue
Block a user