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

564 lines
17 KiB
Rust

use std::path::Path;
use std::sync::Arc;
use galaxy_core::ui::theme::{Fill, GalaxyTheme};
use galaxyui::elements::{
Border, ChildAnchor, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment, Expanded,
Flex, Hoverable, MainAxisAlignment, MainAxisSize, MouseStateHandle, OffsetPositioning,
ParentAnchor, ParentElement, ParentOffsetBounds, Radius, Stack, Text,
};
use galaxyui::fonts::{Properties, Weight};
use galaxyui::geometry::vector::Vector2F;
use galaxyui::platform::Cursor;
use galaxyui::ui_components::components::UiComponent;
use galaxyui::{Element, EventContext};
use pathfinder_color::ColorU;
use pathfinder_geometry::vector::vec2f;
use crate::appearance::Appearance;
use crate::tab_configs::session_config::SessionType;
use crate::ui_components::blended_colors;
use crate::view_components::callout_bubble::{
callout_checkbox, callout_label_color, phenomenon_accent_color, phenomenon_background_color,
phenomenon_body_text_color, phenomenon_disabled_label_text_color, phenomenon_foreground_color,
phenomenon_subtle_border_color,
};
const PILL_GAP: f32 = 8.;
fn session_type_item_color(
is_selected: bool,
on_accent_bg: bool,
theme: &GalaxyTheme,
bg_fill: Fill,
) -> ColorU {
if on_accent_bg {
if is_selected {
phenomenon_background_color()
} else {
phenomenon_body_text_color()
}
} else if is_selected {
blended_colors::text_main(theme, bg_fill)
} else {
blended_colors::text_sub(theme, bg_fill)
}
}
/// Renders the session type pill selector.
///
/// Each pill dispatches `on_select(index)` when clicked.
pub fn render_session_type_pills<F>(
session_types: &[SessionType],
selected_index: usize,
pill_mouse_states: &[MouseStateHandle],
on_select: F,
appearance: &Appearance,
) -> Box<dyn Element>
where
F: Fn(usize, &mut EventContext, Vector2F) + 'static,
{
render_session_type_pills_with_background(
session_types,
selected_index,
pill_mouse_states,
on_select,
None,
appearance,
)
}
/// Renders session type pills with an optional background color override.
/// When `bg` is `Some`, text and border colors are computed against that background
/// (used for the accent-tinted onboarding callout).
pub fn render_session_type_pills_with_background<F>(
session_types: &[SessionType],
selected_index: usize,
pill_mouse_states: &[MouseStateHandle],
on_select: F,
bg: Option<ColorU>,
appearance: &Appearance,
) -> Box<dyn Element>
where
F: Fn(usize, &mut EventContext, Vector2F) + 'static,
{
let theme = appearance.theme();
let bg_fill = bg.map(Fill::Solid).unwrap_or(theme.background());
let on_accent_bg = bg.is_some();
let on_select = Arc::new(on_select);
let label = Text::new_inline("Session type".to_string(), appearance.ui_font_family(), 12.)
.with_color(if on_accent_bg {
callout_label_color(appearance)
} else {
blended_colors::text_disabled(theme, bg_fill)
})
.finish();
let mut pills_row = Flex::row().with_spacing(PILL_GAP);
for (i, session_type) in session_types.iter().enumerate() {
let is_selected = i == selected_index;
let mouse_state = pill_mouse_states[i].clone();
let item_color = session_type_item_color(is_selected, on_accent_bg, theme, bg_fill);
let icon = ConstrainedBox::new(
session_type
.icon()
.to_galaxyui_icon(item_color.into())
.finish(),
)
.with_width(14.)
.with_height(14.)
.finish();
let name = Text::new_inline(
session_type.pill_label().to_string(),
appearance.ui_font_family(),
14.,
)
.with_color(item_color)
.finish();
let pill_content = Flex::row()
.with_main_axis_size(MainAxisSize::Max)
.with_main_axis_alignment(MainAxisAlignment::Center)
.with_cross_axis_alignment(CrossAxisAlignment::Center)
.with_child(icon)
.with_child(Container::new(name).with_margin_left(8.).finish())
.finish();
let border_color = if is_selected {
if on_accent_bg {
phenomenon_accent_color()
} else {
theme.accent().into_solid()
}
} else if on_accent_bg {
phenomenon_subtle_border_color()
} else {
blended_colors::neutral_4(theme)
};
let background = if is_selected {
if on_accent_bg {
Some(Fill::Solid(phenomenon_foreground_color()))
} else {
Some(blended_colors::accent_overlay_1(theme))
}
} else {
None
};
let mut pill = Container::new(pill_content)
.with_horizontal_padding(12.)
.with_vertical_padding(8.)
.with_corner_radius(CornerRadius::with_all(Radius::Pixels(4.)))
.with_border(Border::all(1.).with_border_color(border_color));
if let Some(bg) = background {
pill = pill.with_background(bg);
}
let on_select = on_select.clone();
let pill_element = Expanded::new(
1.0,
Hoverable::new(mouse_state, move |_| pill.finish())
.with_cursor(Cursor::PointingHand)
.on_click(move |ctx, _, position| {
on_select(i, ctx, position);
})
.finish(),
);
pills_row.extend([pill_element.finish()]);
}
Flex::column()
.with_cross_axis_alignment(CrossAxisAlignment::Stretch)
.with_child(label)
.with_child(
Container::new(pills_row.finish())
.with_margin_top(4.)
.finish(),
)
.finish()
}
/// Renders the directory picker button.
///
/// Displays the selected directory in a bordered button. Calls `on_click` when pressed.
pub fn render_directory_picker<F>(
selected_directory: &Path,
mouse_state: MouseStateHandle,
on_click: F,
appearance: &Appearance,
) -> Box<dyn Element>
where
F: Fn(&mut EventContext, Vector2F) + 'static,
{
render_directory_picker_with_background(
selected_directory,
mouse_state,
on_click,
None,
appearance,
)
}
/// Renders a directory picker with an optional background color override.
pub fn render_directory_picker_with_background<F>(
selected_directory: &Path,
mouse_state: MouseStateHandle,
on_click: F,
bg: Option<ColorU>,
appearance: &Appearance,
) -> Box<dyn Element>
where
F: Fn(&mut EventContext, Vector2F) + 'static,
{
let theme = appearance.theme();
let bg_fill = bg.map(Fill::Solid).unwrap_or(theme.background());
let on_accent_bg = bg.is_some();
let label = Text::new_inline(
"Select directory".to_string(),
appearance.ui_font_family(),
12.,
)
.with_color(if on_accent_bg {
callout_label_color(appearance)
} else {
blended_colors::text_disabled(theme, bg_fill)
})
.finish();
let home_dir = dirs::home_dir();
let raw_path = selected_directory.to_string_lossy();
let dir_display = galaxy_util::path::user_friendly_path(
&raw_path,
home_dir.as_ref().and_then(|h| h.to_str()),
)
.into_owned();
let dir_text = Text::new_inline(dir_display, appearance.ui_font_family(), 14.)
.with_color(if on_accent_bg {
phenomenon_body_text_color()
} else {
blended_colors::text_main(theme, bg_fill)
})
.with_style(Properties::default().weight(Weight::Semibold))
.finish();
let border_color = if on_accent_bg {
phenomenon_subtle_border_color()
} else {
blended_colors::neutral_4(theme)
};
let button = Hoverable::new(mouse_state, move |_| {
let content_row = Flex::row()
.with_cross_axis_alignment(CrossAxisAlignment::Center)
.with_child(dir_text)
.finish();
Container::new(ConstrainedBox::new(content_row).with_height(30.).finish())
.with_horizontal_padding(12.)
.with_corner_radius(CornerRadius::with_all(Radius::Pixels(4.)))
.with_border(Border::all(1.).with_border_color(border_color))
.finish()
})
.with_cursor(Cursor::PointingHand)
.on_click(move |ctx, _, position| {
on_click(ctx, position);
})
.finish();
Flex::column()
.with_cross_axis_alignment(CrossAxisAlignment::Stretch)
.with_child(label)
.with_child(Container::new(button).with_margin_top(4.).finish())
.finish()
}
/// Renders the worktree checkbox with label and tooltip.
///
/// `on_toggle` is called when the checkbox is clicked (only when enabled).
pub fn render_worktree_checkbox<F>(
enabled: bool,
is_git_repo: bool,
checkbox_mouse_state: MouseStateHandle,
tooltip_mouse_state: MouseStateHandle,
on_toggle: F,
appearance: &Appearance,
) -> Box<dyn Element>
where
F: Fn(&mut EventContext, Vector2F) + 'static,
{
render_worktree_checkbox_with_background(
enabled,
is_git_repo,
checkbox_mouse_state,
tooltip_mouse_state,
on_toggle,
None,
appearance,
)
}
/// Renders a worktree checkbox with an optional background color override.
pub fn render_worktree_checkbox_with_background<F>(
enabled: bool,
is_git_repo: bool,
checkbox_mouse_state: MouseStateHandle,
tooltip_mouse_state: MouseStateHandle,
on_toggle: F,
bg: Option<ColorU>,
appearance: &Appearance,
) -> Box<dyn Element>
where
F: Fn(&mut galaxyui::EventContext, galaxyui::geometry::vector::Vector2F) + 'static,
{
let disabled = !is_git_repo;
let on_accent_bg = bg.is_some();
let mut checkbox = if on_accent_bg {
callout_checkbox(checkbox_mouse_state, Some(10.5), appearance).check(enabled)
} else {
appearance
.ui_builder()
.checkbox(checkbox_mouse_state, Some(10.5))
.check(enabled)
};
if disabled {
checkbox = checkbox.disabled();
}
let checkbox_el = if disabled {
checkbox.build().finish()
} else {
checkbox
.build()
.with_cursor(Cursor::PointingHand)
.on_click(move |ctx, _, position| {
on_toggle(ctx, position);
})
.finish()
};
let checkbox_el = if disabled {
let theme = appearance.theme();
let font_family = appearance.ui_font_family();
Hoverable::new(tooltip_mouse_state, move |state| {
let mut stack = Stack::new();
stack.add_child(checkbox_el);
if state.is_hovered() {
let tooltip = Container::new(
Text::new_inline(
"Select a git repository to enable worktree support".to_string(),
font_family,
12.,
)
.with_color(theme.background().into_solid())
.finish(),
)
.with_horizontal_padding(14.)
.with_vertical_padding(6.)
.with_corner_radius(CornerRadius::with_all(Radius::Pixels(4.)))
.with_border(Border::all(1.).with_border_fill(theme.outline()))
.with_background_color(theme.tooltip_background())
.finish();
stack.add_positioned_overlay_child(
tooltip,
OffsetPositioning::offset_from_parent(
vec2f(0., -4.),
ParentOffsetBounds::WindowByPosition,
ParentAnchor::TopLeft,
ChildAnchor::BottomLeft,
),
);
}
stack.finish()
})
.finish()
} else {
checkbox_el
};
let theme = appearance.theme();
let label_color = if on_accent_bg {
if disabled {
phenomenon_disabled_label_text_color()
} else {
callout_label_color(appearance)
}
} else if disabled {
blended_colors::text_disabled(theme, theme.background())
} else {
blended_colors::text_sub(theme, theme.background())
};
let label = Text::new(
"Automatically create a worktree when opening a new tab",
appearance.ui_font_family(),
12.,
)
.with_color(label_color)
.finish();
Flex::row()
.with_cross_axis_alignment(CrossAxisAlignment::Center)
.with_child(checkbox_el)
.with_child(Container::new(label).with_margin_left(8.).finish())
.finish()
}
/// Renders the "Autogenerate worktree branch name" checkbox with label and tooltip.
pub fn render_autogenerate_worktree_branch_name_checkbox<F>(
checked: bool,
enable_worktree: bool,
checkbox_mouse_state: MouseStateHandle,
tooltip_mouse_state: MouseStateHandle,
on_toggle: F,
appearance: &Appearance,
) -> Box<dyn Element>
where
F: Fn(&mut EventContext, Vector2F) + 'static,
{
render_autogenerate_worktree_branch_name_checkbox_with_background(
checked,
enable_worktree,
checkbox_mouse_state,
tooltip_mouse_state,
on_toggle,
None,
appearance,
)
}
/// Renders the autogenerate checkbox with an optional background color override.
pub fn render_autogenerate_worktree_branch_name_checkbox_with_background<F>(
checked: bool,
enable_worktree: bool,
checkbox_mouse_state: MouseStateHandle,
tooltip_mouse_state: MouseStateHandle,
on_toggle: F,
bg: Option<ColorU>,
appearance: &Appearance,
) -> Box<dyn Element>
where
F: Fn(&mut EventContext, Vector2F) + 'static,
{
let disabled = !enable_worktree;
let on_accent_bg = bg.is_some();
let mut checkbox = if on_accent_bg {
callout_checkbox(checkbox_mouse_state, Some(10.5), appearance).check(checked)
} else {
appearance
.ui_builder()
.checkbox(checkbox_mouse_state, Some(10.5))
.check(checked)
};
if disabled {
checkbox = checkbox.disabled();
}
let checkbox_el = if disabled {
checkbox.build().finish()
} else {
checkbox
.build()
.with_cursor(Cursor::PointingHand)
.on_click(move |ctx, _, position| {
on_toggle(ctx, position);
})
.finish()
};
let checkbox_el = if disabled {
let theme = appearance.theme();
let font_family = appearance.ui_font_family();
Hoverable::new(tooltip_mouse_state, move |state| {
let mut stack = Stack::new();
stack.add_child(checkbox_el);
if state.is_hovered() {
let tooltip = Container::new(
Text::new_inline(
"You must select that you want to automatically create a \
worktree in order to select this"
.to_string(),
font_family,
12.,
)
.with_color(theme.background().into_solid())
.finish(),
)
.with_horizontal_padding(14.)
.with_vertical_padding(6.)
.with_corner_radius(CornerRadius::with_all(Radius::Pixels(4.)))
.with_border(Border::all(1.).with_border_fill(theme.outline()))
.with_background_color(theme.tooltip_background())
.finish();
stack.add_positioned_overlay_child(
tooltip,
OffsetPositioning::offset_from_parent(
vec2f(0., -4.),
ParentOffsetBounds::WindowByPosition,
ParentAnchor::TopLeft,
ChildAnchor::BottomLeft,
),
);
}
stack.finish()
})
.finish()
} else {
checkbox_el
};
let theme = appearance.theme();
let label_color = if on_accent_bg {
if disabled {
phenomenon_disabled_label_text_color()
} else {
callout_label_color(appearance)
}
} else if disabled {
blended_colors::text_disabled(theme, theme.background())
} else {
blended_colors::text_sub(theme, theme.background())
};
let label = Text::new(
"Auto-generate worktree branch name",
appearance.ui_font_family(),
12.,
)
.with_color(label_color)
.finish();
Flex::row()
.with_cross_axis_alignment(CrossAxisAlignment::Center)
.with_child(checkbox_el)
.with_child(Container::new(label).with_margin_left(8.).finish())
.finish()
}
/// All possible session types, in display order.
const ALL_SESSION_TYPES: &[SessionType] = &[SessionType::Oz, SessionType::Terminal];
/// Returns the session types to display, filtering out Oz when AI is disabled.
pub fn visible_session_types(show_oz: bool) -> Vec<SessionType> {
ALL_SESSION_TYPES
.iter()
.filter(|st| show_oz || !matches!(st, SessionType::Oz))
.copied()
.collect()
}