Files
galaxy/app/src/terminal/input/suggestions_mode_menu.rs
T

391 lines
15 KiB
Rust

//! Rendering logic for the input suggestions menu.
//!
//! This module contains all rendering functions for the various input suggestion modes:
//! - HistoryUp
//! - CompletionSuggestions
//! - StaticWorkflowEnumSuggestions
//! - DynamicWorkflowEnumSuggestions
use super::{
DynamicEnumSuggestionStatus, Input, InputAction, MenuPositioning, DYNAMIC_ENUM_FAILURE_MESSAGE,
DYNAMIC_ENUM_GENERATE_MESSAGE, DYNAMIC_ENUM_HORIZONTAL_TEXT_PADDING,
DYNAMIC_ENUM_MENU_HEIGHT_OFFSET, DYNAMIC_ENUM_MENU_PADDING, DYNAMIC_ENUM_NO_RESULTS_MESSAGE,
DYNAMIC_ENUM_PENDING_MESSAGE, DYNAMIC_ENUM_RUN_MESSAGE, HISTORY_DETAILS_VIEW_WIDTH_REQUIREMENT,
RUN_DYNAMIC_ENUM_COMMAND_KEYSTROKE, TERMINAL_VIEW_PADDING_LEFT,
};
use crate::appearance::Appearance;
use crate::input_suggestions::{
DETAILS_PANEL_MARGIN, DETAILS_PANEL_PADDING, HISTORY_DETAILS_PANEL_WIDTH,
LABEL_PADDING as InputSuggestionsLabelPadding,
};
use crate::themes::theme::WarpTheme;
use galaxyui::elements::{
Align, Border, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment, DragBarSide,
DropShadow, Element, Empty, Flex, ParentElement, Radius, Resizable, Shrinkable,
SizeConstraintCondition, SizeConstraintSwitch, Text,
};
use galaxyui::presenter::ChildView;
use galaxyui::ui_components::components::{Coords, UiComponent, UiComponentStyles};
enum SuggestionsResizeConfig {
WidthAndHeight,
WidthOnly,
HeightOnly,
}
impl Input {
pub(super) fn render_history_up_menu(
&self,
appearance: &Appearance,
menu_positioning: MenuPositioning,
) -> Box<dyn Element> {
let theme = appearance.theme();
let radius = Radius::Pixels(6.);
let corner_radius = match menu_positioning {
MenuPositioning::AboveInputBox => CornerRadius::with_top(radius),
MenuPositioning::BelowInputBox => CornerRadius::with_bottom(radius),
};
let input_suggestions_border = 2.0;
let margin =
*TERMINAL_VIEW_PADDING_LEFT - input_suggestions_border - InputSuggestionsLabelPadding;
let content = ChildView::new(&self.input_suggestions).finish();
SizeConstraintSwitch::new(
Flex::row()
.with_children([
Shrinkable::new(
1.,
self.render_suggestions_container(
margin,
corner_radius,
theme,
SuggestionsResizeConfig::HeightOnly,
menu_positioning,
content,
),
)
.finish(),
ConstrainedBox::new(Empty::new().finish())
.with_width(
HISTORY_DETAILS_PANEL_WIDTH
+ DETAILS_PANEL_MARGIN * 2.
+ DETAILS_PANEL_PADDING * 2.,
)
.finish(),
])
.finish(),
vec![(
SizeConstraintCondition::WidthLessThan(HISTORY_DETAILS_VIEW_WIDTH_REQUIREMENT),
self.render_suggestions_container(
margin,
corner_radius,
theme,
SuggestionsResizeConfig::HeightOnly,
menu_positioning,
ChildView::new(&self.input_suggestions).finish(),
),
)],
)
.finish()
}
pub(super) fn render_completion_suggestions_menu(
&self,
appearance: &Appearance,
menu_positioning: MenuPositioning,
) -> Box<dyn Element> {
let theme = appearance.theme();
let corner_radius = CornerRadius::with_all(Radius::Pixels(6.));
let content = ChildView::new(&self.input_suggestions).finish();
self.render_suggestions_container(
0.,
corner_radius,
theme,
SuggestionsResizeConfig::WidthAndHeight,
menu_positioning,
content,
)
}
pub(super) fn render_workflow_enum_suggestions_menu(
&self,
appearance: &Appearance,
menu_positioning: MenuPositioning,
) -> Box<dyn Element> {
let theme = appearance.theme();
let corner_radius = CornerRadius::with_all(Radius::Pixels(6.));
// Same styling as CompletionSuggestions
let margin = 0.;
let content = ChildView::new(&self.input_suggestions).finish();
self.render_suggestions_container(
margin,
corner_radius,
theme,
SuggestionsResizeConfig::WidthAndHeight,
menu_positioning,
content,
)
}
pub(super) fn render_dynamic_workflow_enum_menu(
&self,
appearance: &Appearance,
menu_positioning: MenuPositioning,
command: String,
status: DynamicEnumSuggestionStatus,
suggestions: &[String],
) -> Box<dyn Element> {
let theme = appearance.theme();
let corner_radius = CornerRadius::with_all(Radius::Pixels(6.));
let (content, resize_config) = match status {
DynamicEnumSuggestionStatus::Unapproved => (
self.render_dynamic_enum_command_approval(command, appearance),
SuggestionsResizeConfig::WidthOnly,
),
DynamicEnumSuggestionStatus::Pending => (
self.render_dynamic_enum_status_message(DYNAMIC_ENUM_PENDING_MESSAGE, appearance),
SuggestionsResizeConfig::WidthAndHeight,
),
DynamicEnumSuggestionStatus::Failure => (
self.render_dynamic_enum_status_message(DYNAMIC_ENUM_FAILURE_MESSAGE, appearance),
SuggestionsResizeConfig::WidthAndHeight,
),
DynamicEnumSuggestionStatus::Success if suggestions.is_empty() => (
self.render_dynamic_enum_status_message(
DYNAMIC_ENUM_NO_RESULTS_MESSAGE,
appearance,
),
SuggestionsResizeConfig::WidthAndHeight,
),
DynamicEnumSuggestionStatus::Success => (
ChildView::new(&self.input_suggestions).finish(),
SuggestionsResizeConfig::WidthAndHeight,
),
};
self.render_suggestions_container(
0.,
corner_radius,
theme,
resize_config,
menu_positioning,
content,
)
}
/// Renders the suggestions container with configurable resize behavior.
fn render_suggestions_container(
&self,
margin: f32,
corner_radius: CornerRadius,
theme: &WarpTheme,
resize_config: SuggestionsResizeConfig,
menu_positioning: MenuPositioning,
content: Box<dyn Element>,
) -> Box<dyn Element> {
// Inner container for the visual menu content (background, border, shadow)
// This is wrapped by Resizable so the resize handles align with the border.
let inner_container = Container::new(content)
.with_background(theme.surface_2())
.with_corner_radius(corner_radius)
.with_drop_shadow(DropShadow::default())
.with_border(Border::all(1.0).with_border_fill(theme.outline()))
.finish();
// Apply width resizing based on config
let horizontal_resizable = match resize_config {
SuggestionsResizeConfig::WidthAndHeight | SuggestionsResizeConfig::WidthOnly => {
let width_handle_for_end = self.completions_menu_resizable_width.clone();
Resizable::new(
self.completions_menu_resizable_width.clone(),
inner_container,
)
.with_dragbar_side(DragBarSide::Right)
.with_dragbar_offset(7.0)
.with_bounds_callback(Box::new(|window_size| (200.0, window_size.x())))
.on_resize(move |ctx, _| {
ctx.notify();
})
.on_end_resizing(move |ctx, _| {
let new_width = width_handle_for_end
.lock()
.expect("width handle lock poisoned")
.size();
ctx.dispatch_typed_action(InputAction::UpdateCompletionsMenuWidth(new_width));
})
.finish()
}
SuggestionsResizeConfig::HeightOnly => inner_container,
};
// Apply height resizing based on config
// Skip vertical resizing for command approval to show full command because we want users
// to see the entire command they are about to execute.
let resizable_element = match resize_config {
SuggestionsResizeConfig::WidthOnly => horizontal_resizable,
SuggestionsResizeConfig::WidthAndHeight | SuggestionsResizeConfig::HeightOnly => {
let dragbar_side = match menu_positioning {
MenuPositioning::AboveInputBox => DragBarSide::Top,
MenuPositioning::BelowInputBox => DragBarSide::Bottom,
};
let height_handle_for_end = self.completions_menu_resizable_height.clone();
Resizable::new(
self.completions_menu_resizable_height.clone(),
horizontal_resizable,
)
.with_dragbar_side(dragbar_side)
.with_bounds_callback(Box::new(|window_size| (100.0, window_size.y())))
.on_resize(move |ctx, _| {
ctx.notify();
})
.on_end_resizing(move |ctx, _| {
let new_height = height_handle_for_end
.lock()
.expect("height handle lock poisoned")
.size();
ctx.dispatch_typed_action(InputAction::UpdateCompletionsMenuHeight(new_height));
})
.finish()
}
};
Container::new(resizable_element)
.with_margin_top(12.0)
.with_margin_left(margin)
.with_margin_right(margin)
.finish()
}
/// Renders the command approval UI for dynamic enum suggestions.
fn render_dynamic_enum_command_approval(
&self,
command: String,
appearance: &Appearance,
) -> Box<dyn Element> {
let theme = appearance.theme();
let keybinding_style = UiComponentStyles {
height: Some(15.),
font_size: Some(appearance.ui_font_size()),
margin: Some(Coords {
left: 2.,
right: 2.,
..Default::default()
}),
..Default::default()
};
Flex::column()
.with_child(
ConstrainedBox::new(
Container::new(
Text::new_inline(
String::from(DYNAMIC_ENUM_GENERATE_MESSAGE),
appearance.ui_font_family(),
appearance.ui_font_size(),
)
.with_color(theme.main_text_color(theme.surface_2()).into_solid())
.finish(),
)
.with_uniform_padding(DYNAMIC_ENUM_MENU_PADDING)
.finish(),
)
.with_max_height(appearance.ui_font_size() + DYNAMIC_ENUM_MENU_HEIGHT_OFFSET)
.finish(),
)
.with_child(
Container::new(
Text::new(
command.clone(),
appearance.monospace_font_family(),
appearance.monospace_font_size(),
)
.with_color(theme.main_text_color(theme.background()).into_solid())
.finish(),
)
.with_uniform_padding(DYNAMIC_ENUM_MENU_PADDING)
.with_background(theme.background())
.finish(),
)
.with_child(
ConstrainedBox::new(
Flex::row()
.with_child(
Shrinkable::new(
1.,
Container::new(
appearance
.ui_builder()
.keyboard_shortcut(&RUN_DYNAMIC_ENUM_COMMAND_KEYSTROKE)
.with_style(keybinding_style)
.build()
.finish(),
)
.with_uniform_padding(DYNAMIC_ENUM_HORIZONTAL_TEXT_PADDING)
.finish(),
)
.finish(),
)
.with_child(
Shrinkable::new(
1.,
Container::new(
Text::new_inline(
String::from(DYNAMIC_ENUM_RUN_MESSAGE),
appearance.ui_font_family(),
appearance.ui_font_size(),
)
.with_color(theme.sub_text_color(theme.surface_2()).into())
.finish(),
)
.with_uniform_padding(DYNAMIC_ENUM_HORIZONTAL_TEXT_PADDING)
.finish(),
)
.finish(),
)
.with_cross_axis_alignment(CrossAxisAlignment::Center)
.finish(),
)
.with_max_height(appearance.ui_font_size() + DYNAMIC_ENUM_MENU_HEIGHT_OFFSET)
.finish(),
)
.finish()
}
/// Renders a status message for dynamic enum suggestions.
fn render_dynamic_enum_status_message(
&self,
message: &str,
appearance: &Appearance,
) -> Box<dyn Element> {
let theme = appearance.theme();
ConstrainedBox::new(
Align::new(
Container::new(
Text::new_inline(
String::from(message),
appearance.monospace_font_family(),
appearance.monospace_font_size(),
)
.with_color(theme.sub_text_color(theme.surface_2()).into())
.finish(),
)
.with_uniform_padding(DYNAMIC_ENUM_HORIZONTAL_TEXT_PADDING)
.finish(),
)
.finish(),
)
.with_max_height(appearance.monospace_font_size() + DYNAMIC_ENUM_MENU_HEIGHT_OFFSET)
.finish()
}
}