Files
galaxy/app/src/tab_configs/session_config_modal.rs
T

390 lines
14 KiB
Rust

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<String, galaxyui::platform::file_picker::FilePickerError>),
ToggleWorktree,
ToggleAutogenerateWorktreeBranchName,
Submit,
Dismiss,
}
pub enum SessionConfigModalEvent {
Completed(SessionConfigSelection),
Dismissed,
}
pub struct SessionConfigModal {
session_types: Vec<SessionType>,
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<MouseStateHandle>,
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<ActionButton>,
submit_button: ViewHandle<ActionButton>,
}
impl SessionConfigModal {
pub fn new(ctx: &mut ViewContext<Self>) -> 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<Self>) {
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<dyn Element> {
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<dyn Element> {
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<dyn Element> {
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<dyn Element> {
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<dyn Element> {
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<Self>) {
ctx.focus_self();
}
fn render(&self, app: &AppContext) -> Box<dyn Element> {
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<Self>) {
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);
}
}
}
}