203 lines · 6.7 KB · Rust
RawDownload
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<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_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()    }}