use pathfinder_color::ColorU; use pathfinder_geometry::vector::Vector2F; use warp_core::ui::Icon; use warp_core::ui::appearance::Appearance; use warpui::elements::{ Align, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment, DropShadow, Flex, MainAxisAlignment, ParentElement as _, Radius, Shrinkable, }; use warpui::fonts::Weight; use warpui::ui_components::components::{BorderStyle, Coords, UiComponent as _, UiComponentStyles}; use warpui::{ AppContext, Element, Entity, FocusContext, SingletonEntity as _, TypedActionView, View, ViewContext, ViewHandle, }; 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, /// A closure that returns if the current editor content are valid and can be submitted. validator: Box bool>, /// Whether or not the last submission attempt failed validation. has_error: bool, } impl GlowingEditor { pub fn new(placeholder: impl Into, ctx: &mut ViewContext) -> 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 bool + 'static>(mut self, validator: F) -> Self { self.validator = Box::new(validator); self } pub fn clear_buffer(&mut self, ctx: &mut ViewContext) { self.editor.update(ctx, |editor, ctx| { editor.clear_buffer(ctx); }); } fn handle_editor_event(&mut self, event: &EditorEvent, ctx: &mut ViewContext) { 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) { if focus_ctx.is_self_focused() { ctx.focus(&self.editor); } } fn render(&self, app: &AppContext) -> Box { 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_warpui_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() } }