rust_widgets 0.9.6

Pure Rust cross-platform native GUI library with hardware-adaptive rendering, 60+ widgets, touch/gesture support, i18n, and SVG-pipeline-accurate output
//! Rich text editor widget.
use crate::core::Rect;
use crate::render::RenderContext;
use crate::signal::Signal1;
use crate::widget::{BaseWidget, Draw, Widget, WidgetKind};
/// Rich text/code editor baseline widget contract.
pub struct RichEdit {
    base: BaseWidget,
    text: String,
    selection: Option<(usize, usize)>,
    read_only: bool,
    pub text_changed: Signal1<String>,
    pub selection_changed: Signal1<Option<(usize, usize)>>,
    pub read_only_changed: Signal1<bool>,
    pub cursor_position_changed: Signal1<usize>,
}
impl RichEdit {
    /// Creates an empty rich editor.
    pub fn new(geometry: Rect) -> Self {
        Self {
            base: BaseWidget::new(WidgetKind::RichEdit, geometry, "RichEdit"),
            text: String::new(),
            selection: None,
            read_only: false,
            text_changed: Signal1::new(),
            selection_changed: Signal1::new(),
            read_only_changed: Signal1::new(),
            cursor_position_changed: Signal1::new(),
        }
    }
    /// Returns current editor text.
    pub fn text(&self) -> &str {
        &self.text
    }
    /// Replaces editor text and resets selection/cursor to end.
    pub fn set_text(&mut self, text: String) {
        if self.read_only || self.text == text {
            return;
        }
        self.text = text;
        self.selection = None;
        self.text_changed.emit(self.text.clone());
        self.cursor_position_changed.emit(self.text.len());
    }
    /// Returns current selection range.
    pub fn selection(&self) -> Option<(usize, usize)> {
        self.selection
    }
    /// Sets selection range.
    pub fn set_selection(&mut self, start: usize, end: usize) {
        if self.read_only {
            return;
        }
        let start = start.min(self.text.len());
        let end = end.min(self.text.len());
        if self.selection == Some((start, end)) {
            return;
        }
        self.selection = Some((start, end));
        self.selection_changed.emit(self.selection);
    }
    /// Clears selection.
    pub fn clear_selection(&mut self) {
        if self.selection.is_none() {
            return;
        }
        self.selection = None;
        self.selection_changed.emit(None);
    }
    /// Returns read-only state.
    pub fn is_read_only(&self) -> bool {
        self.read_only
    }
    /// Sets read-only state.
    pub fn set_read_only(&mut self, read_only: bool) {
        if self.read_only == read_only {
            return;
        }
        self.read_only = read_only;
        self.read_only_changed.emit(read_only);
    }
    /// Returns cursor position.
    pub fn cursor_position(&self) -> usize {
        self.selection.map_or(0, |(start, _)| start)
    }
    /// Sets cursor position.
    pub fn set_cursor_position(&mut self, position: usize) {
        if self.read_only {
            return;
        }
        let position = position.min(self.text.len());
        self.selection = Some((position, position));
        self.cursor_position_changed.emit(position);
    }
}
impl Widget for RichEdit {
    fn base(&self) -> &BaseWidget {
        &self.base
    }
    fn base_mut(&mut self) -> &mut BaseWidget {
        &mut self.base
    }
}
impl Draw for RichEdit {
    fn draw(&mut self, context: &mut RenderContext) {
        let rect = self.base.geometry();
        use crate::core::Color;
        // Draw background
        context.fill_rect(rect, Color::from_rgb(255, 255, 255));
        // Draw border
        context.draw_rect(
            rect,
            if self.read_only {
                Color::from_rgb(220, 220, 220)
            } else {
                Color::from_rgb(180, 180, 180)
            },
        );
        // Draw text content (first line only as preview)
        if !self.text.is_empty() {
            let line = self.text.lines().next().unwrap_or("");
            context.draw_text(
                crate::core::Point::new(rect.x + 2, rect.y + rect.height as i32 / 2),
                line,
                &crate::core::Font::default(),
                Color::from_rgb(0, 0, 0),
            );
        }
    }
}

impl crate::event::EventHandler for RichEdit {
    fn handle_event(&mut self, event: &crate::event::Event) {
        if !self.base.is_enabled() || self.read_only {
            return;
        }
        match event {
            crate::event::Event::MousePress { pos: _, button } if *button == 1 => {
                self.base.set_mouse_pressed(true);
            }
            crate::event::Event::MouseRelease { pos: _, button } if *button == 1 => {
                self.base.set_mouse_pressed(false);
            }
            _ => { /* Other events are not relevant */ }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::core::Rect;

    #[test]
    fn richedit_creation_defaults() {
        let re = RichEdit::new(Rect::new(0, 0, 400, 300));
        assert!(re.text().is_empty());
        assert!(re.selection().is_none());
        assert!(!re.is_read_only());
        assert_eq!(re.cursor_position(), 0);
    }

    #[test]
    fn richedit_set_text() {
        let mut re = RichEdit::new(Rect::new(0, 0, 400, 300));
        re.set_text("Hello RichEdit".to_string());
        assert_eq!(re.text(), "Hello RichEdit");
    }

    #[test]
    fn richedit_set_text_read_only() {
        let mut re = RichEdit::new(Rect::new(0, 0, 400, 300));
        re.set_read_only(true);
        re.set_text("Should not change".to_string());
        assert!(re.text().is_empty());
    }

    #[test]
    fn richedit_read_only() {
        let mut re = RichEdit::new(Rect::new(0, 0, 400, 300));
        assert!(!re.is_read_only());
        re.set_read_only(true);
        assert!(re.is_read_only());
        re.set_read_only(false);
        assert!(!re.is_read_only());
    }

    #[test]
    fn richedit_set_selection() {
        let mut re = RichEdit::new(Rect::new(0, 0, 400, 300));
        re.set_text("Hello World".to_string());
        re.set_selection(0, 5);
        assert_eq!(re.selection(), Some((0, 5)));
    }

    #[test]
    fn richedit_clear_selection() {
        let mut re = RichEdit::new(Rect::new(0, 0, 400, 300));
        re.set_text("Hello World".to_string());
        re.set_selection(0, 5);
        re.clear_selection();
        assert!(re.selection().is_none());
    }

    #[test]
    fn richedit_set_cursor_position() {
        let mut re = RichEdit::new(Rect::new(0, 0, 400, 300));
        re.set_text("Hello".to_string());
        re.set_cursor_position(3);
        assert_eq!(re.cursor_position(), 3);
        re.set_cursor_position(100); // clamps
        assert_eq!(re.cursor_position(), 5);
    }

    #[test]
    fn richedit_geometry_delegation() {
        let mut re = RichEdit::new(Rect::new(0, 0, 400, 300));
        re.set_geometry(Rect::new(10, 10, 500, 400));
        assert_eq!(re.geometry(), Rect::new(10, 10, 500, 400));
    }

    #[test]
    fn richedit_visibility() {
        let mut re = RichEdit::new(Rect::new(0, 0, 400, 300));
        assert!(re.is_visible());
        re.hide();
        assert!(!re.is_visible());
        re.show();
        assert!(re.is_visible());
    }

    #[test]
    fn richedit_enabled() {
        let mut re = RichEdit::new(Rect::new(0, 0, 400, 300));
        assert!(re.is_enabled());
        re.set_enabled(false);
        assert!(!re.is_enabled());
        re.set_enabled(true);
        assert!(re.is_enabled());
    }

    #[test]
    fn richedit_id_kind() {
        let re_a = RichEdit::new(Rect::new(0, 0, 100, 100));
        let re_b = RichEdit::new(Rect::new(0, 0, 100, 100));
        assert_ne!(re_a.id(), re_b.id());
        assert_eq!(re_a.kind(), WidgetKind::RichEdit);
        assert_eq!(re_b.kind(), WidgetKind::RichEdit);
    }

    #[test]
    fn richedit_signal_accessors() {
        let re = RichEdit::new(Rect::new(0, 0, 100, 100));
        let _ = &re.text_changed;
        let _ = &re.selection_changed;
        let _ = &re.read_only_changed;
        let _ = &re.cursor_position_changed;
    }
}