use std::path::PathBuf; use galaxyui::elements::{ ChildAnchor, ChildView, ConstrainedBox, Container, CrossAxisAlignment, Flex, FormattedTextElement, MouseStateHandle, OffsetPositioning, ParentAnchor, ParentElement, ParentOffsetBounds, Stack, }; use galaxyui::fonts::Weight; use galaxyui::keymap::macros::id; use galaxyui::keymap::{FixedBinding, Keystroke}; use galaxyui::platform::file_picker::FilePickerConfiguration; use galaxyui::{ AppContext, Element, Entity, FocusContext, SingletonEntity, TypedActionView, View, ViewContext, ViewHandle, }; use pathfinder_geometry::vector::vec2f; use super::session_config::{is_git_repo, SessionConfigSelection, SessionType}; use super::session_config_rendering; use crate::appearance::Appearance; use crate::ui_components::blended_colors; use crate::view_components::action_button::{ ActionButton, ButtonSize, KeystrokeSource, NakedTheme, PrimaryTheme, }; pub fn init(app: &mut galaxyui::AppContext) { app.register_fixed_bindings([FixedBinding::new( "enter", SessionConfigModalAction::Submit, id!(SessionConfigModal::ui_name()), )]); } const SECTION_GAP: f32 = 16.; #[derive(Clone, Debug)] pub enum SessionConfigModalAction { SelectSessionType(usize), OpenDirectoryPicker, DirectorySelected(Result), ToggleWorktree, ToggleAutogenerateWorktreeBranchName, Submit, Dismiss, } pub enum SessionConfigModalEvent { Completed(SessionConfigSelection), Dismissed, } pub struct SessionConfigModal { session_types: Vec, selected_session_type_index: usize, selected_directory: PathBuf, is_git_repo: bool, enable_worktree: bool, autogenerate_worktree_branch_name: bool, /// When `false`, the session type pill row is hidden and the session type /// defaults to Terminal behind the scenes (used when Oz is disabled). show_session_type_row: bool, session_pill_mouse_states: Vec, directory_button_mouse_state: MouseStateHandle, worktree_checkbox_mouse_state: MouseStateHandle, autogenerate_worktree_branch_name_checkbox_mouse_state: MouseStateHandle, autogenerate_tooltip_mouse_state: MouseStateHandle, worktree_tooltip_mouse_state: MouseStateHandle, close_button: ViewHandle, submit_button: ViewHandle, } impl SessionConfigModal { pub fn new(ctx: &mut ViewContext) -> Self { let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("/")); let session_types = session_config_rendering::visible_session_types(true); let close_button = ctx.add_view(|ctx| { ActionButton::new("", NakedTheme) .with_icon(crate::ui_components::icons::Icon::X) .with_size(ButtonSize::Small) .with_keybinding( KeystrokeSource::Fixed(Keystroke::parse("escape").unwrap_or_default()), ctx, ) .on_click(|ctx| ctx.dispatch_typed_action(SessionConfigModalAction::Dismiss)) }); let submit_button = ctx.add_view(|ctx| { ActionButton::new("Get Warping", PrimaryTheme) .with_full_width(true) .with_keybinding( KeystrokeSource::Fixed(Keystroke::parse("enter").unwrap_or_default()), ctx, ) .on_click(|ctx| ctx.dispatch_typed_action(SessionConfigModalAction::Submit)) }); let pill_mouse_states = session_types .iter() .map(|_| MouseStateHandle::default()) .collect(); Self { session_types, selected_session_type_index: 0, selected_directory: home, // Filled in by `configure()` before the modal is shown. is_git_repo: false, enable_worktree: false, autogenerate_worktree_branch_name: false, show_session_type_row: true, session_pill_mouse_states: pill_mouse_states, directory_button_mouse_state: MouseStateHandle::default(), worktree_checkbox_mouse_state: MouseStateHandle::default(), autogenerate_worktree_branch_name_checkbox_mouse_state: MouseStateHandle::default(), autogenerate_tooltip_mouse_state: MouseStateHandle::default(), worktree_tooltip_mouse_state: MouseStateHandle::default(), close_button, submit_button, } } /// Reconfigures the visible session types based on whether Oz is available. /// Resets the selection to index 0 (the first available type). /// When Oz is disabled, hides the session type row entirely and defaults /// to Terminal behind the scenes. pub fn configure(&mut self, show_oz: bool) { self.show_session_type_row = show_oz; self.session_types = session_config_rendering::visible_session_types(show_oz); self.selected_session_type_index = 0; self.session_pill_mouse_states = self .session_types .iter() .map(|_| MouseStateHandle::default()) .collect(); self.is_git_repo = is_git_repo(&self.selected_directory); } fn selected_session_type(&self) -> SessionType { self.session_types[self.selected_session_type_index] } fn update_directory(&mut self, path: PathBuf) { self.is_git_repo = is_git_repo(&path); if !self.is_git_repo { self.enable_worktree = false; self.autogenerate_worktree_branch_name = false; } self.selected_directory = path; } fn submit(&mut self, ctx: &mut ViewContext) { ctx.emit(SessionConfigModalEvent::Completed(SessionConfigSelection { session_type: self.selected_session_type(), directory: self.selected_directory.clone(), enable_worktree: self.enable_worktree, autogenerate_worktree_branch_name: self.autogenerate_worktree_branch_name, })); } // ── Rendering ── fn render_header(&self, appearance: &Appearance) -> Box { let theme = appearance.theme(); let title = FormattedTextElement::from_str( "Create your first tab config", appearance.ui_font_family(), 24., ) .with_color(blended_colors::text_main(theme, theme.background())) .with_weight(Weight::Semibold) .finish(); let subtitle_text = if self.show_session_type_row { "Set up a reusable starting point for your tabs. \ Pick a repo, choose a session type, and optionally attach a worktree. \ Use it whenever you want to open a new tab with this setup." } else { "Set up a reusable starting point for your tabs. \ Pick a repo, optionally attach a worktree, and \ use it whenever you want to open a new tab with this setup." }; let subtitle = FormattedTextElement::from_str(subtitle_text, appearance.ui_font_family(), 14.) .with_color(blended_colors::text_sub(theme, theme.background())) .finish(); Flex::column() .with_cross_axis_alignment(CrossAxisAlignment::Start) .with_child(title) .with_child(Container::new(subtitle).with_margin_top(4.).finish()) .finish() } fn render_session_type_section(&self, appearance: &Appearance) -> Box { session_config_rendering::render_session_type_pills( &self.session_types, self.selected_session_type_index, &self.session_pill_mouse_states, |i, ctx, _| { ctx.dispatch_typed_action(SessionConfigModalAction::SelectSessionType(i)); }, appearance, ) } fn render_directory_section(&self, appearance: &Appearance) -> Box { session_config_rendering::render_directory_picker( &self.selected_directory, self.directory_button_mouse_state.clone(), |ctx, _| { ctx.dispatch_typed_action(SessionConfigModalAction::OpenDirectoryPicker); }, appearance, ) } fn render_checkboxes(&self, appearance: &Appearance) -> Box { session_config_rendering::render_worktree_checkbox( self.enable_worktree, self.is_git_repo, self.worktree_checkbox_mouse_state.clone(), self.worktree_tooltip_mouse_state.clone(), |ctx, _| { ctx.dispatch_typed_action(SessionConfigModalAction::ToggleWorktree); }, appearance, ) } fn render_autogenerate_worktree_branch_name_checkbox( &self, appearance: &Appearance, ) -> Box { session_config_rendering::render_autogenerate_worktree_branch_name_checkbox( self.autogenerate_worktree_branch_name, self.enable_worktree, self.autogenerate_worktree_branch_name_checkbox_mouse_state .clone(), self.autogenerate_tooltip_mouse_state.clone(), |ctx, _| { ctx.dispatch_typed_action( SessionConfigModalAction::ToggleAutogenerateWorktreeBranchName, ); }, appearance, ) } } impl Entity for SessionConfigModal { type Event = SessionConfigModalEvent; } impl View for SessionConfigModal { fn ui_name() -> &'static str { "SessionConfigModal" } fn on_focus(&mut self, _focus_ctx: &FocusContext, ctx: &mut ViewContext) { ctx.focus_self(); } fn render(&self, app: &AppContext) -> Box { let appearance = Appearance::as_ref(app); let mut form = Flex::column() .with_cross_axis_alignment(CrossAxisAlignment::Stretch) .with_child(self.render_header(appearance)); if self.show_session_type_row { form.add_child( Container::new(self.render_session_type_section(appearance)) .with_margin_top(SECTION_GAP) .finish(), ); } form.add_child( Container::new(self.render_directory_section(appearance)) .with_margin_top(SECTION_GAP) .finish(), ); form.add_child( Container::new(self.render_checkboxes(appearance)) .with_margin_top(SECTION_GAP) .finish(), ); form.add_child( Container::new(self.render_autogenerate_worktree_branch_name_checkbox(appearance)) .with_margin_top(8.) .finish(), ); let content = Flex::column() .with_cross_axis_alignment(CrossAxisAlignment::Stretch) .with_child(form.finish()) .with_child( Container::new(ChildView::new(&self.submit_button).finish()) .with_margin_top(32.) .finish(), ) .finish(); let body = Container::new(content) .with_horizontal_padding(32.) .with_vertical_padding(40.) .finish(); let mut stack = Stack::new(); stack.add_child(body); stack.add_positioned_overlay_child( Container::new(ChildView::new(&self.close_button).finish()) .with_margin_top(12.) .with_margin_right(12.) .finish(), OffsetPositioning::offset_from_parent( vec2f(0., 0.), ParentOffsetBounds::ParentByPosition, ParentAnchor::TopRight, ChildAnchor::TopRight, ), ); ConstrainedBox::new(stack.finish()) .with_width(420.) .finish() } } impl TypedActionView for SessionConfigModal { type Action = SessionConfigModalAction; fn handle_action(&mut self, action: &Self::Action, ctx: &mut ViewContext) { match action { SessionConfigModalAction::SelectSessionType(index) => { self.selected_session_type_index = *index; ctx.notify(); } SessionConfigModalAction::OpenDirectoryPicker => { ctx.open_file_picker( |result, ctx| { if let Some(path_result) = result.map(|paths| paths.into_iter().next()).transpose() { ctx.dispatch_typed_action( &SessionConfigModalAction::DirectorySelected(path_result), ); } }, FilePickerConfiguration::new().folders_only(), ); } SessionConfigModalAction::DirectorySelected(result) => match result { Ok(path) => { self.update_directory(PathBuf::from(path)); ctx.notify(); } Err(err) => { log::warn!("File picker error in session config modal: {err}"); } }, SessionConfigModalAction::ToggleWorktree => { if self.is_git_repo { self.enable_worktree = !self.enable_worktree; if !self.enable_worktree { self.autogenerate_worktree_branch_name = false; } ctx.notify(); } } SessionConfigModalAction::ToggleAutogenerateWorktreeBranchName => { if self.enable_worktree { self.autogenerate_worktree_branch_name = !self.autogenerate_worktree_branch_name; ctx.notify(); } } SessionConfigModalAction::Submit => { self.submit(ctx); } SessionConfigModalAction::Dismiss => { ctx.emit(SessionConfigModalEvent::Dismissed); } } } }