204 lines
6.6 KiB
Rust
204 lines
6.6 KiB
Rust
use galaxy_core::ui::appearance::Appearance;
|
|
use galaxy_core::ui::Icon;
|
|
use galaxyui::elements::{
|
|
Align, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment, DropShadow, Flex,
|
|
MainAxisAlignment, ParentElement as _, Radius, Shrinkable,
|
|
};
|
|
use galaxyui::fonts::Weight;
|
|
use galaxyui::ui_components::components::{
|
|
BorderStyle, Coords, UiComponent as _, UiComponentStyles,
|
|
};
|
|
use galaxyui::{
|
|
AppContext, Element, Entity, FocusContext, SingletonEntity as _, TypedActionView, View,
|
|
ViewContext, ViewHandle,
|
|
};
|
|
use pathfinder_color::ColorU;
|
|
use pathfinder_geometry::vector::Vector2F;
|
|
|
|
use crate::editor::{EditorOptions, EditorView, Event as EditorEvent, TextOptions};
|
|
|
|
const PROMPT_INPUT_HEIGHT: f32 = 56.;
|
|
const ICON_MARGIN_LEFT: f32 = 12.;
|
|
const ICON_MARGIN_RIGHT: f32 = 6.;
|
|
|
|
pub struct GlowingEditor {
|
|
editor: ViewHandle<EditorView>,
|
|
/// A closure that returns if the current editor content are valid and can be submitted.
|
|
validator: Box<dyn Fn(&str) -> bool>,
|
|
/// Whether or not the last submission attempt failed validation.
|
|
has_error: bool,
|
|
}
|
|
|
|
impl GlowingEditor {
|
|
pub fn new(placeholder: impl Into<String>, ctx: &mut ViewContext<Self>) -> Self {
|
|
let appearance = Appearance::as_ref(ctx);
|
|
let font_family = appearance.ui_font_family();
|
|
let font_size = appearance.ui_font_size() + 2.;
|
|
|
|
let editor = ctx.add_typed_action_view(|ctx| {
|
|
let options = EditorOptions {
|
|
soft_wrap: true,
|
|
text: TextOptions {
|
|
font_size_override: Some(font_size),
|
|
font_family_override: Some(font_family),
|
|
..Default::default()
|
|
},
|
|
..Default::default()
|
|
};
|
|
let mut editor = EditorView::new(options, ctx);
|
|
editor.set_placeholder_text(placeholder, ctx);
|
|
editor
|
|
});
|
|
|
|
ctx.subscribe_to_view(&editor, move |me, _, event, ctx| {
|
|
me.handle_editor_event(event, ctx);
|
|
});
|
|
|
|
Self {
|
|
editor,
|
|
validator: Box::new(|_| true),
|
|
has_error: false,
|
|
}
|
|
}
|
|
|
|
/// Validates the input contents using the provided `validator` whenever a submit action is
|
|
/// attempted.
|
|
#[expect(dead_code, reason = "Nothing needs validation currently.")]
|
|
pub fn with_validator<F: Fn(&str) -> bool + 'static>(mut self, validator: F) -> Self {
|
|
self.validator = Box::new(validator);
|
|
self
|
|
}
|
|
|
|
pub fn clear_buffer(&mut self, ctx: &mut ViewContext<Self>) {
|
|
self.editor.update(ctx, |editor, ctx| {
|
|
editor.clear_buffer(ctx);
|
|
});
|
|
}
|
|
|
|
fn handle_editor_event(&mut self, event: &EditorEvent, ctx: &mut ViewContext<Self>) {
|
|
match event {
|
|
EditorEvent::Enter => {
|
|
let prompt = self
|
|
.editor
|
|
.read(ctx, |editor, ctx| editor.buffer_text(ctx).trim().to_owned());
|
|
if prompt.is_empty() {
|
|
return;
|
|
}
|
|
|
|
if !(self.validator)(&prompt) {
|
|
self.has_error = true;
|
|
ctx.notify();
|
|
} else {
|
|
self.has_error = false;
|
|
self.clear_buffer(ctx);
|
|
ctx.emit(GlowingEditorEvent::Submit(prompt));
|
|
}
|
|
}
|
|
EditorEvent::Escape => ctx.emit(GlowingEditorEvent::Cancel),
|
|
// Clear error state when user types (since this is submit-only validation)
|
|
EditorEvent::Edited(_) if self.has_error => {
|
|
self.has_error = false;
|
|
ctx.notify();
|
|
}
|
|
_ => (),
|
|
}
|
|
}
|
|
}
|
|
|
|
pub enum GlowingEditorEvent {
|
|
Submit(String),
|
|
Cancel,
|
|
}
|
|
|
|
impl Entity for GlowingEditor {
|
|
type Event = GlowingEditorEvent;
|
|
}
|
|
|
|
impl TypedActionView for GlowingEditor {
|
|
type Action = ();
|
|
}
|
|
|
|
impl View for GlowingEditor {
|
|
fn ui_name() -> &'static str {
|
|
"GlowingEditor"
|
|
}
|
|
|
|
fn on_focus(&mut self, focus_ctx: &FocusContext, ctx: &mut ViewContext<Self>) {
|
|
if focus_ctx.is_self_focused() {
|
|
ctx.focus(&self.editor);
|
|
}
|
|
}
|
|
|
|
fn render(&self, app: &AppContext) -> Box<dyn Element> {
|
|
let appearance = Appearance::as_ref(app);
|
|
let theme = appearance.theme();
|
|
|
|
let input_box = Shrinkable::new(
|
|
1.,
|
|
Align::new(
|
|
appearance
|
|
.ui_builder()
|
|
.text_input(self.editor.clone())
|
|
.with_style(UiComponentStyles {
|
|
font_weight: Some(Weight::Semibold),
|
|
border_style: Some(BorderStyle::None),
|
|
border_width: Some(0.),
|
|
background: Some(ColorU::transparent_black().into()),
|
|
padding: Some(Coords::uniform(10.).left(0.)),
|
|
..Default::default()
|
|
})
|
|
.build()
|
|
.finish(),
|
|
)
|
|
.finish(),
|
|
)
|
|
.finish();
|
|
|
|
let font_size = appearance.ui_font_size() + 2.;
|
|
let agent_icon = Container::new(
|
|
ConstrainedBox::new(
|
|
Icon::AgentMode
|
|
.to_galaxyui_icon(theme.sub_text_color(theme.background()))
|
|
.finish(),
|
|
)
|
|
.with_height(font_size)
|
|
.with_width(font_size)
|
|
.finish(),
|
|
)
|
|
.with_margin_left(ICON_MARGIN_LEFT)
|
|
.with_margin_right(ICON_MARGIN_RIGHT)
|
|
.finish();
|
|
|
|
let editor_content = ConstrainedBox::new(
|
|
Flex::row()
|
|
.with_cross_axis_alignment(CrossAxisAlignment::Center)
|
|
.with_main_axis_alignment(MainAxisAlignment::Start)
|
|
.with_children([agent_icon, input_box])
|
|
.finish(),
|
|
)
|
|
.with_min_height(PROMPT_INPUT_HEIGHT);
|
|
|
|
let border_fill = if self.has_error {
|
|
theme.ui_error_color()
|
|
} else {
|
|
theme.outline().into_solid()
|
|
};
|
|
|
|
let shadow_color = if self.has_error {
|
|
ColorU::new(255, 0, 0, 100) // Red shadow with higher opacity for error
|
|
} else {
|
|
ColorU::new(255, 143, 253, 15) // Default purple shadow
|
|
};
|
|
|
|
Container::new(editor_content.finish())
|
|
.with_border(border_fill)
|
|
.with_corner_radius(CornerRadius::with_all(Radius::Pixels(4.)))
|
|
.with_drop_shadow(
|
|
DropShadow::new_with_standard_offset_and_spread(shadow_color)
|
|
.with_offset(Vector2F::zero()),
|
|
)
|
|
.with_background(theme.background().into_solid())
|
|
.finish()
|
|
}
|
|
}
|