486 lines
18 KiB
Rust
486 lines
18 KiB
Rust
use crate::appearance::Appearance;
|
|
use crate::ui_components::icons::Icon;
|
|
use pathfinder_color::ColorU;
|
|
use pathfinder_geometry::vector::vec2f;
|
|
use galaxy_core::ui::color::blend::Blend;
|
|
use galaxy_core::ui::theme::color::internal_colors;
|
|
use galaxyui::elements::{
|
|
Align, Border, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment, Dismiss,
|
|
DropShadow, Element, Flex, Hoverable, MainAxisAlignment, MainAxisSize, MouseStateHandle,
|
|
ParentElement, Radius, Shrinkable, Text,
|
|
};
|
|
use galaxyui::fonts::{Properties, Weight};
|
|
use galaxyui::keymap::{FixedBinding, Keystroke};
|
|
use galaxyui::platform::Cursor;
|
|
use galaxyui::ui_components::components::{Coords, UiComponent, UiComponentStyles};
|
|
use galaxyui::{AppContext, Entity, SingletonEntity, TypedActionView, View, ViewContext};
|
|
|
|
// Figma node 6583:23542
|
|
const MODAL_WIDTH: f32 = 441.;
|
|
const DIALOG_CORNER_RADIUS: f32 = 8.;
|
|
|
|
const HEADER_PADDING_TOP: f32 = 16.;
|
|
const HEADER_PADDING_BOTTOM: f32 = 16.;
|
|
const HEADER_PADDING_HORIZONTAL: f32 = 24.;
|
|
|
|
const BODY_PADDING_VERTICAL: f32 = 16.;
|
|
const BODY_PADDING_HORIZONTAL: f32 = 20.;
|
|
|
|
const OPTION_PADDING_VERTICAL: f32 = 8.;
|
|
const OPTION_PADDING_HORIZONTAL: f32 = 12.;
|
|
const OPTION_CORNER_RADIUS: f32 = 4.;
|
|
const OPTION_GAP: f32 = 12.;
|
|
const OPTIONS_VERTICAL_GAP: f32 = 8.;
|
|
|
|
const AVATAR_SIZE: f32 = 48.;
|
|
const AVATAR_ICON_SIZE: f32 = 24.;
|
|
|
|
const TITLE_FONT_SIZE: f32 = 16.;
|
|
const OPTION_TITLE_FONT_SIZE: f32 = 14.;
|
|
const OPTION_DESC_FONT_SIZE: f32 = 12.;
|
|
|
|
/// The mode for environment setup selected by the user.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum EnvironmentSetupMode {
|
|
/// Use remote GitHub repositories (form-based flow).
|
|
RemoteGitHub,
|
|
/// Use local repositories (agent-based flow).
|
|
LocalRepositories,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub enum EnvironmentSetupModeSelectorAction {
|
|
SelectRemoteGitHub,
|
|
SelectLocalRepositories,
|
|
Dismiss,
|
|
HoveredIn(usize),
|
|
ArrowUp,
|
|
ArrowDown,
|
|
Enter,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub enum EnvironmentSetupModeSelectorEvent {
|
|
Selected(EnvironmentSetupMode),
|
|
Dismissed,
|
|
}
|
|
|
|
pub struct EnvironmentSetupModeSelector {
|
|
close_button_mouse_state: MouseStateHandle,
|
|
remote_github_mouse_state: MouseStateHandle,
|
|
local_repos_mouse_state: MouseStateHandle,
|
|
selected_option_index: usize,
|
|
}
|
|
|
|
pub fn init(app: &mut AppContext) {
|
|
use galaxyui::keymap::macros::*;
|
|
|
|
app.register_fixed_bindings(vec![
|
|
FixedBinding::new(
|
|
"escape",
|
|
EnvironmentSetupModeSelectorAction::Dismiss,
|
|
id!("EnvironmentSetupModeSelector"),
|
|
),
|
|
FixedBinding::new(
|
|
"enter",
|
|
EnvironmentSetupModeSelectorAction::Enter,
|
|
id!("EnvironmentSetupModeSelector"),
|
|
),
|
|
FixedBinding::new(
|
|
"numpadenter",
|
|
EnvironmentSetupModeSelectorAction::Enter,
|
|
id!("EnvironmentSetupModeSelector"),
|
|
),
|
|
FixedBinding::new(
|
|
"up",
|
|
EnvironmentSetupModeSelectorAction::ArrowUp,
|
|
id!("EnvironmentSetupModeSelector"),
|
|
),
|
|
FixedBinding::new(
|
|
"down",
|
|
EnvironmentSetupModeSelectorAction::ArrowDown,
|
|
id!("EnvironmentSetupModeSelector"),
|
|
),
|
|
FixedBinding::new(
|
|
"tab",
|
|
EnvironmentSetupModeSelectorAction::ArrowDown,
|
|
id!("EnvironmentSetupModeSelector"),
|
|
),
|
|
FixedBinding::new(
|
|
"shift-tab",
|
|
EnvironmentSetupModeSelectorAction::ArrowUp,
|
|
id!("EnvironmentSetupModeSelector"),
|
|
),
|
|
]);
|
|
}
|
|
|
|
impl EnvironmentSetupModeSelector {
|
|
pub fn new(_ctx: &mut ViewContext<Self>) -> Self {
|
|
Self {
|
|
close_button_mouse_state: MouseStateHandle::default(),
|
|
remote_github_mouse_state: MouseStateHandle::default(),
|
|
local_repos_mouse_state: MouseStateHandle::default(),
|
|
selected_option_index: 0,
|
|
}
|
|
}
|
|
|
|
fn render_header(&self, appearance: &Appearance) -> Box<dyn Element> {
|
|
let theme = appearance.theme();
|
|
|
|
let title = Text::new(
|
|
"Choose how you'd like to set up your environment".to_string(),
|
|
appearance.ui_font_family(),
|
|
TITLE_FONT_SIZE,
|
|
)
|
|
.with_style(Properties::default().weight(Weight::Bold))
|
|
.with_color(theme.active_ui_text_color().into())
|
|
.soft_wrap(true)
|
|
.finish();
|
|
|
|
let close_button = appearance
|
|
.ui_builder()
|
|
.close_button(16., self.close_button_mouse_state.clone())
|
|
.build()
|
|
.on_click(|ctx, _, _| {
|
|
ctx.dispatch_typed_action(EnvironmentSetupModeSelectorAction::Dismiss);
|
|
})
|
|
.finish();
|
|
|
|
let esc_keystroke = Keystroke::parse("escape").expect("escape keystroke parses");
|
|
let esc_pill = appearance
|
|
.ui_builder()
|
|
.keyboard_shortcut(&esc_keystroke)
|
|
.with_style(UiComponentStyles {
|
|
font_size: Some(OPTION_DESC_FONT_SIZE),
|
|
font_color: Some(theme.nonactive_ui_text_color().into_solid()),
|
|
border_radius: Some(CornerRadius::with_all(Radius::Pixels(4.))),
|
|
padding: Some(Coords {
|
|
top: 0.,
|
|
bottom: 0.,
|
|
left: 3.,
|
|
right: 3.,
|
|
}),
|
|
height: Some(16.),
|
|
..Default::default()
|
|
})
|
|
.build()
|
|
.finish();
|
|
|
|
let right_controls = Flex::row()
|
|
.with_cross_axis_alignment(CrossAxisAlignment::Center)
|
|
.with_child(Container::new(esc_pill).with_margin_right(8.).finish())
|
|
.with_child(close_button)
|
|
.finish();
|
|
|
|
Flex::row()
|
|
.with_main_axis_size(MainAxisSize::Max)
|
|
.with_main_axis_alignment(MainAxisAlignment::SpaceBetween)
|
|
.with_cross_axis_alignment(CrossAxisAlignment::Start)
|
|
.with_spacing(12.)
|
|
.with_child(Shrinkable::new(1., title).finish())
|
|
.with_child(right_controls)
|
|
.finish()
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
fn render_option(
|
|
&self,
|
|
index: usize,
|
|
icon: Icon,
|
|
title: &'static str,
|
|
description: &'static str,
|
|
is_suggested: bool,
|
|
mouse_state: MouseStateHandle,
|
|
action: EnvironmentSetupModeSelectorAction,
|
|
appearance: &Appearance,
|
|
) -> Box<dyn Element> {
|
|
let theme = appearance.theme();
|
|
|
|
let font_family = appearance.ui_font_family();
|
|
let active_text = theme.active_ui_text_color();
|
|
let nonactive_text = theme.nonactive_ui_text_color();
|
|
|
|
let base_background = theme.surface_2();
|
|
let hover_background = base_background.blend(&internal_colors::accent_overlay_1(theme));
|
|
|
|
let base_border = internal_colors::neutral_4(theme);
|
|
let hover_border = theme.accent().into_solid();
|
|
|
|
// Avatar styling - lighter background for the circular icon container
|
|
let avatar_background = internal_colors::neutral_2(theme);
|
|
let avatar_border = internal_colors::neutral_3(theme);
|
|
|
|
// Badge styling - lighter colors
|
|
let badge_background = internal_colors::neutral_2(theme);
|
|
let badge_border = internal_colors::neutral_3(theme);
|
|
let badge_text_color = internal_colors::neutral_5(theme);
|
|
|
|
let icon_color = nonactive_text;
|
|
|
|
let is_selected = self.selected_option_index == index;
|
|
let action = action.clone();
|
|
Hoverable::new(mouse_state, move |state| {
|
|
let is_hovered = state.is_hovered() || state.is_clicked();
|
|
let (background, border_color) = if is_hovered || is_selected {
|
|
(hover_background, hover_border)
|
|
} else {
|
|
(base_background, base_border)
|
|
};
|
|
|
|
let avatar_icon = ConstrainedBox::new(icon.to_galaxyui_icon(icon_color).finish())
|
|
.with_width(AVATAR_ICON_SIZE)
|
|
.with_height(AVATAR_ICON_SIZE)
|
|
.finish();
|
|
|
|
let avatar_contents = ConstrainedBox::new(
|
|
Flex::row()
|
|
.with_main_axis_alignment(MainAxisAlignment::Center)
|
|
.with_cross_axis_alignment(CrossAxisAlignment::Center)
|
|
.with_main_axis_size(MainAxisSize::Max)
|
|
.with_child(avatar_icon)
|
|
.finish(),
|
|
)
|
|
.with_width(AVATAR_SIZE)
|
|
.with_height(AVATAR_SIZE)
|
|
.finish();
|
|
|
|
let avatar = Container::new(avatar_contents)
|
|
.with_corner_radius(CornerRadius::with_all(Radius::Percentage(50.)))
|
|
.with_background(avatar_background)
|
|
.with_border(Border::all(1.).with_border_color(avatar_border))
|
|
.finish();
|
|
|
|
let title_text = Text::new(title.to_string(), font_family, OPTION_TITLE_FONT_SIZE)
|
|
.with_style(Properties::default().weight(Weight::Semibold))
|
|
.with_color(active_text.into())
|
|
.finish();
|
|
|
|
let mut title_row = Flex::row()
|
|
.with_cross_axis_alignment(CrossAxisAlignment::Center)
|
|
.with_spacing(4.)
|
|
.with_child(title_text);
|
|
|
|
if is_suggested {
|
|
let suggested_text =
|
|
Text::new("Suggested".to_string(), font_family, OPTION_DESC_FONT_SIZE)
|
|
.with_style(Properties::default().weight(Weight::Medium))
|
|
.with_color(badge_text_color)
|
|
.finish();
|
|
|
|
let suggested = Container::new(suggested_text)
|
|
.with_horizontal_padding(8.)
|
|
.with_vertical_padding(2.)
|
|
.with_corner_radius(CornerRadius::with_all(Radius::Percentage(50.)))
|
|
.with_background(badge_background)
|
|
.with_border(Border::all(1.).with_border_color(badge_border))
|
|
.finish();
|
|
|
|
title_row.add_child(Container::new(suggested).with_margin_left(8.).finish());
|
|
}
|
|
|
|
let description_text =
|
|
Text::new(description.to_string(), font_family, OPTION_DESC_FONT_SIZE)
|
|
.with_style(Properties::default().weight(Weight::Normal))
|
|
.with_color(nonactive_text.into())
|
|
.soft_wrap(true)
|
|
.finish();
|
|
|
|
let text_content = Flex::column()
|
|
.with_cross_axis_alignment(CrossAxisAlignment::Start)
|
|
.with_child(title_row.finish())
|
|
.with_child(
|
|
Container::new(description_text)
|
|
.with_margin_top(4.)
|
|
.finish(),
|
|
)
|
|
.finish();
|
|
|
|
Container::new(
|
|
Flex::row()
|
|
.with_cross_axis_alignment(CrossAxisAlignment::Center)
|
|
.with_spacing(OPTION_GAP)
|
|
.with_child(avatar)
|
|
.with_child(Shrinkable::new(1., text_content).finish())
|
|
.finish(),
|
|
)
|
|
.with_padding_left(OPTION_PADDING_HORIZONTAL)
|
|
.with_padding_right(OPTION_PADDING_HORIZONTAL)
|
|
.with_padding_top(OPTION_PADDING_VERTICAL)
|
|
.with_padding_bottom(OPTION_PADDING_VERTICAL)
|
|
.with_corner_radius(CornerRadius::with_all(Radius::Pixels(OPTION_CORNER_RADIUS)))
|
|
.with_border(Border::all(1.).with_border_color(border_color))
|
|
.with_background(background)
|
|
.finish()
|
|
})
|
|
.with_cursor(Cursor::PointingHand)
|
|
.additional_on_hover(move |is_hovered, ctx, _app, _pos| {
|
|
if is_hovered {
|
|
ctx.dispatch_typed_action(EnvironmentSetupModeSelectorAction::HoveredIn(index));
|
|
}
|
|
})
|
|
.on_click(move |ctx, _, _| {
|
|
ctx.dispatch_typed_action(action.clone());
|
|
})
|
|
.finish()
|
|
}
|
|
|
|
fn render_modal(&self, app: &AppContext) -> Box<dyn Element> {
|
|
let appearance = Appearance::as_ref(app);
|
|
let theme = appearance.theme();
|
|
|
|
let header = Container::new(self.render_header(appearance))
|
|
.with_padding_top(HEADER_PADDING_TOP)
|
|
.with_padding_bottom(HEADER_PADDING_BOTTOM)
|
|
.with_padding_left(HEADER_PADDING_HORIZONTAL)
|
|
.with_padding_right(HEADER_PADDING_HORIZONTAL)
|
|
.finish();
|
|
|
|
let remote_github_option = self.render_option(
|
|
0,
|
|
Icon::Github,
|
|
"Quick setup",
|
|
"Select the GitHub repositories you'd like to work with and we'll suggest a base image and config",
|
|
true,
|
|
self.remote_github_mouse_state.clone(),
|
|
EnvironmentSetupModeSelectorAction::SelectRemoteGitHub,
|
|
appearance,
|
|
);
|
|
|
|
let local_repos_option = self.render_option(
|
|
1,
|
|
Icon::Terminal,
|
|
"Use the agent",
|
|
"Choose a locally set up project and we'll help you set up an environment based on it",
|
|
false,
|
|
self.local_repos_mouse_state.clone(),
|
|
EnvironmentSetupModeSelectorAction::SelectLocalRepositories,
|
|
appearance,
|
|
);
|
|
|
|
let options = Flex::column()
|
|
.with_cross_axis_alignment(CrossAxisAlignment::Stretch)
|
|
.with_spacing(OPTIONS_VERTICAL_GAP)
|
|
.with_child(remote_github_option)
|
|
.with_child(local_repos_option)
|
|
.finish();
|
|
|
|
let body = Container::new(options)
|
|
.with_padding_top(BODY_PADDING_VERTICAL)
|
|
.with_padding_bottom(BODY_PADDING_VERTICAL)
|
|
.with_padding_left(BODY_PADDING_HORIZONTAL)
|
|
.with_padding_right(BODY_PADDING_HORIZONTAL)
|
|
.finish();
|
|
|
|
let dialog_contents = Flex::column()
|
|
.with_cross_axis_alignment(CrossAxisAlignment::Stretch)
|
|
.with_child(header)
|
|
.with_child(body)
|
|
.finish();
|
|
|
|
let dialog_background = theme
|
|
.surface_1()
|
|
.blend(&internal_colors::fg_overlay_1(theme));
|
|
let dialog_border = internal_colors::neutral_4(theme);
|
|
|
|
let dialog = Container::new(dialog_contents)
|
|
.with_corner_radius(CornerRadius::with_all(Radius::Pixels(DIALOG_CORNER_RADIUS)))
|
|
.with_border(Border::all(1.).with_border_color(dialog_border))
|
|
.with_background(dialog_background)
|
|
.with_drop_shadow(DropShadow {
|
|
color: ColorU::new(0, 0, 0, 77),
|
|
offset: vec2f(0., 7.),
|
|
blur_radius: 7.,
|
|
spread_radius: 0.,
|
|
})
|
|
.finish();
|
|
|
|
let constrained_dialog = ConstrainedBox::new(dialog).with_width(MODAL_WIDTH).finish();
|
|
|
|
let dismiss_dialog = Dismiss::new(constrained_dialog)
|
|
.prevent_interaction_with_other_elements()
|
|
.on_dismiss(|ctx, _app| {
|
|
ctx.dispatch_typed_action(EnvironmentSetupModeSelectorAction::Dismiss);
|
|
})
|
|
.finish();
|
|
|
|
Container::new(Align::new(dismiss_dialog).finish())
|
|
.with_background(appearance.theme().dark_overlay())
|
|
.finish()
|
|
}
|
|
}
|
|
|
|
impl Entity for EnvironmentSetupModeSelector {
|
|
type Event = EnvironmentSetupModeSelectorEvent;
|
|
}
|
|
|
|
impl TypedActionView for EnvironmentSetupModeSelector {
|
|
type Action = EnvironmentSetupModeSelectorAction;
|
|
|
|
fn handle_action(&mut self, action: &Self::Action, ctx: &mut ViewContext<Self>) {
|
|
match action {
|
|
EnvironmentSetupModeSelectorAction::SelectRemoteGitHub => {
|
|
ctx.emit(EnvironmentSetupModeSelectorEvent::Selected(
|
|
EnvironmentSetupMode::RemoteGitHub,
|
|
));
|
|
ctx.notify();
|
|
}
|
|
EnvironmentSetupModeSelectorAction::SelectLocalRepositories => {
|
|
ctx.emit(EnvironmentSetupModeSelectorEvent::Selected(
|
|
EnvironmentSetupMode::LocalRepositories,
|
|
));
|
|
ctx.notify();
|
|
}
|
|
EnvironmentSetupModeSelectorAction::Dismiss => {
|
|
ctx.emit(EnvironmentSetupModeSelectorEvent::Dismissed);
|
|
ctx.notify();
|
|
}
|
|
EnvironmentSetupModeSelectorAction::HoveredIn(index) => {
|
|
self.selected_option_index = *index;
|
|
ctx.notify();
|
|
}
|
|
EnvironmentSetupModeSelectorAction::ArrowUp => {
|
|
self.selected_option_index = if self.selected_option_index == 0 {
|
|
1
|
|
} else {
|
|
0
|
|
};
|
|
ctx.notify();
|
|
}
|
|
EnvironmentSetupModeSelectorAction::ArrowDown => {
|
|
self.selected_option_index = if self.selected_option_index == 0 {
|
|
1
|
|
} else {
|
|
0
|
|
};
|
|
ctx.notify();
|
|
}
|
|
EnvironmentSetupModeSelectorAction::Enter => {
|
|
match self.selected_option_index {
|
|
0 => {
|
|
ctx.emit(EnvironmentSetupModeSelectorEvent::Selected(
|
|
EnvironmentSetupMode::RemoteGitHub,
|
|
));
|
|
}
|
|
1 => {
|
|
ctx.emit(EnvironmentSetupModeSelectorEvent::Selected(
|
|
EnvironmentSetupMode::LocalRepositories,
|
|
));
|
|
}
|
|
_ => {}
|
|
}
|
|
ctx.notify();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl View for EnvironmentSetupModeSelector {
|
|
fn ui_name() -> &'static str {
|
|
"EnvironmentSetupModeSelector"
|
|
}
|
|
|
|
fn render(&self, app: &AppContext) -> Box<dyn Element> {
|
|
self.render_modal(app)
|
|
}
|
|
}
|