Files
galaxy/app/src/workspace/rewind_confirmation_dialog.rs
T

271 lines
9.1 KiB
Rust

use galaxy_core::ui::{color::coloru_with_opacity, theme::Fill};
use galaxyui::{
elements::{
Align, ChildAnchor, ConstrainedBox, Container, CrossAxisAlignment, Flex, Hoverable,
MouseStateHandle, OffsetPositioning, ParentAnchor, ParentElement, ParentOffsetBounds,
Stack, Text,
},
fonts::Weight,
keymap::{FixedBinding, Keystroke},
platform::Cursor,
ui_components::{
button::ButtonVariant,
components::{Coords, UiComponent, UiComponentStyles},
},
AppContext, Element, Entity, EntityId, SingletonEntity, TypedActionView, View, ViewContext,
};
use pathfinder_geometry::vector::vec2f;
use crate::{
ai::agent::{conversation::AIConversationId, AIAgentExchangeId},
appearance::Appearance,
ui_components::dialog::{dialog_styles, Dialog},
ui_components::icons::Icon,
};
pub fn init(app: &mut AppContext) {
use galaxyui::keymap::macros::*;
app.register_fixed_bindings([
FixedBinding::new(
"escape",
RewindConfirmationAction::Cancel,
id!(RewindConfirmationDialog::ui_name()),
),
FixedBinding::new(
"enter",
RewindConfirmationAction::Confirm,
id!(RewindConfirmationDialog::ui_name()),
),
]);
}
const DIALOG_WIDTH: f32 = 460.;
/// Data needed to perform the rewind action after confirmation
#[derive(Clone)]
pub struct RewindDialogSource {
pub ai_block_view_id: EntityId,
pub exchange_id: AIAgentExchangeId,
pub conversation_id: AIConversationId,
}
pub struct RewindConfirmationDialog {
cancel_mouse_state: MouseStateHandle,
confirm_mouse_state: MouseStateHandle,
/// Source will be None if dialog was never opened
rewind_source: Option<RewindDialogSource>,
}
impl Default for RewindConfirmationDialog {
fn default() -> Self {
Self::new()
}
}
impl RewindConfirmationDialog {
pub fn new() -> Self {
Self {
cancel_mouse_state: Default::default(),
confirm_mouse_state: Default::default(),
rewind_source: None,
}
}
pub fn set_rewind_source(&mut self, source: RewindDialogSource) {
self.rewind_source = Some(source);
}
}
impl Entity for RewindConfirmationDialog {
type Event = RewindConfirmationEvent;
}
impl View for RewindConfirmationDialog {
fn ui_name() -> &'static str {
"RewindConfirmationDialog"
}
fn on_focus(&mut self, _focus_ctx: &galaxyui::FocusContext, ctx: &mut ViewContext<Self>) {
ctx.focus_self();
}
fn render(&self, app: &AppContext) -> Box<dyn Element> {
let appearance = Appearance::as_ref(app);
let theme = appearance.theme();
let button_style = UiComponentStyles {
font_size: Some(14.),
font_weight: Some(Weight::Bold),
height: Some(40.),
..Default::default()
};
// Build rewind button label with Enter keyboard shortcut indicator
let enter_keystroke = Keystroke::parse("enter").expect("Valid keystroke");
let text_color = theme.main_text_color(theme.accent()).into_solid();
let rewind_button_label = Flex::row()
.with_cross_axis_alignment(CrossAxisAlignment::Center)
.with_child(
Text::new_inline("Rewind", appearance.ui_font_family(), 14.)
.with_color(text_color)
.finish(),
)
.with_child(
Container::new(
appearance
.ui_builder()
.keyboard_shortcut(&enter_keystroke)
.with_style(UiComponentStyles {
font_size: Some(10.),
height: Some(16.),
padding: Some(Coords::uniform(1.)),
border_width: Some(1.),
border_color: Some(coloru_with_opacity(text_color, 60).into()),
font_color: Some(text_color),
font_family_id: Some(appearance.ui_font_family()),
..Default::default()
})
.with_line_height_ratio(1.0)
.build()
.finish(),
)
.with_margin_left(8.)
.finish(),
)
.finish();
let rewind_button = appearance
.ui_builder()
.button(ButtonVariant::Accent, self.confirm_mouse_state.clone())
.with_custom_label(rewind_button_label)
.with_style(UiComponentStyles {
padding: Some(Coords {
left: 16.,
right: 16.,
top: 0.,
bottom: 0.,
}),
..button_style
})
.build()
.with_cursor(Cursor::PointingHand)
.on_click(move |ctx, _, _| ctx.dispatch_typed_action(RewindConfirmationAction::Confirm))
.finish();
let cancel_text_color = theme.sub_text_color(theme.surface_2());
let cancel_button = Container::new(
Hoverable::new(self.cancel_mouse_state.clone(), move |mouse_state| {
let color = if mouse_state.is_mouse_over_element() {
theme.main_text_color(theme.surface_2())
} else {
cancel_text_color
};
Text::new_inline("Cancel", appearance.ui_font_family(), 14.)
.with_color(color.into_solid())
.finish()
})
.with_cursor(Cursor::PointingHand)
.on_click(move |ctx, _, _| ctx.dispatch_typed_action(RewindConfirmationAction::Cancel))
.finish(),
)
.with_margin_right(16.)
.finish();
// Info text with icon
let info_color = theme.sub_text_color(theme.surface_2());
let info_row = Flex::row()
.with_cross_axis_alignment(CrossAxisAlignment::Center)
.with_child(
Container::new(
ConstrainedBox::new(Icon::Info.to_galaxyui_icon(info_color).finish())
.with_height(14.)
.with_width(14.)
.finish(),
)
.with_margin_right(6.)
.finish(),
)
.with_child(
Text::new_inline(
"Rewinding does not affect files edited manually or via shell commands.",
appearance.ui_font_family(),
12.,
)
.with_color(info_color.into_solid())
.finish(),
)
.finish();
let dialog = Container::new(
Dialog::new(
"Rewind".into(),
Some(
"Are you sure you want to rewind? This will restore your code and conversation to before this point, and cancel any commands the agent is currently running. A copy of the original conversation will be saved in your conversation history."
.into(),
),
UiComponentStyles {
width: Some(DIALOG_WIDTH),
padding: Some(Coords::uniform(24.)),
..dialog_styles(appearance)
},
)
.with_child(info_row)
.with_bottom_row_child(cancel_button)
.with_bottom_row_child(rewind_button)
.build()
.finish(),
)
.with_margin_top(35.)
.finish();
// Stack needed so that dialog can get bounds information
let mut stack = Stack::new();
stack.add_positioned_child(
dialog,
OffsetPositioning::offset_from_parent(
vec2f(0., 0.),
ParentOffsetBounds::WindowByPosition,
ParentAnchor::Center,
ChildAnchor::Center,
),
);
// This blurs the background and makes it uninteractable
Container::new(Align::new(stack.finish()).finish())
.with_background_color(Fill::blur().into())
.with_corner_radius(app.windows().window_corner_radius())
.finish()
}
}
pub enum RewindConfirmationEvent {
Confirm { rewind_source: RewindDialogSource },
Cancel,
}
#[derive(Debug)]
pub enum RewindConfirmationAction {
Confirm,
Cancel,
}
impl TypedActionView for RewindConfirmationDialog {
type Action = RewindConfirmationAction;
fn handle_action(&mut self, action: &RewindConfirmationAction, ctx: &mut ViewContext<Self>) {
match action {
RewindConfirmationAction::Confirm => {
let Some(rewind_source) = self.rewind_source.clone() else {
log::error!("Rewind confirm button pressed with no rewind source");
return;
};
ctx.emit(RewindConfirmationEvent::Confirm { rewind_source });
}
RewindConfirmationAction::Cancel => {
ctx.emit(RewindConfirmationEvent::Cancel);
}
}
}
}