pelican_ui_std 0.2.17

Standard UI tools and components built off Pelican UI of the RAMP stack.
use pelican_ui::events::{MouseState, MouseEvent, OnEvent, Event, TickEvent, Key, NamedKey};
use pelican_ui::layout::{Area, SizeRequest, Layout};
use pelican_ui::drawable::{Drawable, Component, Shape, Color, Align, Span, Cursor};
use pelican_ui::drawable::Text as BasicText;
use pelican_ui::{Context, Component, resources};

use crate::layout::{Stack, Offset, Size, Padding, Opt, Row, Column};
use crate::components::{Rectangle, Circle};

/// # Text Style
///
/// Represents the different text styles supported by Pelican UI.
#[derive(Clone, Copy, Debug)]
pub enum TextStyle {
    /// Text style for large text, titles, and headings.
    Heading,
    /// Text style for standard primary text.
    Primary,
    /// Text style for less prominent text, like subtitles or hints.
    Secondary,
    /// Text style to indicate errors or warnings.
    Error,
    /// Text style for active text inputs.
    White,
    /// Text style for keyboard keys.
    Keyboard,
    /// Text style for button labels with a specified colors.
    Label(Color),
}


impl TextStyle {
    pub fn get(&self, ctx: &mut Context) -> (Color, resources::Font) {
        let theme = &ctx.theme;
        match self {
            TextStyle::Heading => (theme.colors.text.heading, theme.fonts.fonts.heading.clone()),
            TextStyle::Primary => (theme.colors.text.primary, theme.fonts.fonts.text.clone()),
            TextStyle::Secondary => (theme.colors.text.secondary, theme.fonts.fonts.text.clone()),
            TextStyle::Error => (theme.colors.status.danger, theme.fonts.fonts.text.clone()),
            TextStyle::White => (Color::WHITE, theme.fonts.fonts.text.clone()),
            TextStyle::Keyboard => (theme.colors.text.heading, theme.fonts.fonts.keyboard.clone()),
            TextStyle::Label(color) => (*color, theme.fonts.fonts.label.clone()),
        }
    }
}

#[derive(Component, Debug)]
pub struct Text(Stack, BasicText);
impl OnEvent for Text {}

impl Text {
    // TODO add max lines to this as well
    pub fn new(ctx: &mut Context, text: &str, style: TextStyle, size: f32, align: Align) -> Self {
        let (color, font) = style.get(ctx);
        let text = BasicText::new(vec![Span::new(text.to_string(), size, Some(size*1.25), font, color, 0.0)], None, align, None);
        Text(Stack(Offset::Start, Offset::Start, Size::Fit, Size::Fit, Padding::default()), text)
    }

    pub fn set_kerning(&mut self, kerning: f32) {
        self.1.spans[0].kerning = kerning;
    }

    pub fn text(&mut self) -> &mut BasicText { &mut self.1 }
}

/// # Expandable Text
///
/// A text component that expands to take as much horizontal space as possible,  
/// enabling automatic line wrapping and custom text alignment.  
/// Unlike [`Text`], which only sizes to fit its content.
///
/// <img src="https://raw.githubusercontent.com/ramp-stack/pelican_ui_std/main/src/examples/expandable_text.png"
///      alt="Expandable Text Example"
///      width="400">
///
/// ## Example
/// ```rust
/// let text = "Greyhounds are gentle, affectionate dogs that love to run.
/// They are known for their incredible speed and calm temperament,
/// making them excellent companions.";
///
/// let text_size = ctx.theme.fonts.size.md;
///
/// let expandable = ExpandableText::new(
///     ctx,
///     text,
///     TextStyle::Primary,
///     text_size,
///     Align::Start,
///     Some(3), // limit to 3 lines before truncation
/// );
/// ```

#[derive(Debug)]
pub struct ExpandableText(pub Text);
impl OnEvent for ExpandableText {}

impl ExpandableText {
    pub fn new(ctx: &mut Context, text: &str, style: TextStyle, size: f32, align: Align, max_lines: Option<u32>) -> Self {
        let (color, font) = style.get(ctx);
        let text = BasicText::new(vec![Span::new(text.to_string(), size, Some(size*1.25), font, color, 0.0)], None, align, max_lines);
        ExpandableText(Text(Stack(Offset::Start, Offset::Start, Size::Fit, Size::Fit, Padding::default()), text))
    }

    pub fn set_kerning(&mut self, kerning: f32) { self.0.set_kerning(kerning); }

    pub fn text(&mut self) -> &mut BasicText { self.0.text() }
}

impl Component for ExpandableText {
    fn children_mut(&mut self) -> Vec<&mut dyn Drawable> { vec![&mut self.0] }
    fn children(&self) -> Vec<&dyn Drawable> { vec![&self.0] }

    fn request_size(&self, _ctx: &mut Context, children: Vec<SizeRequest>) -> SizeRequest {
        SizeRequest::new(0.0, children[0].min_height(), f32::MAX, children[0].max_height())
    }

    fn build(&mut self, _ctx: &mut Context, size: (f32, f32), _children: Vec<SizeRequest>) -> Vec<Area> {
        self.0.text().width = Some(size.0);
        vec![Area{offset: (0.0, 0.0), size}]
    }
}

#[derive(Component, Debug)]
pub struct TextEditor(Stack, ExpandableText, TextCursor);

impl TextEditor {
    pub fn new(ctx: &mut Context, text: &str, style: TextStyle, size: f32, align: Align) -> Self {
        let mut text = ExpandableText::new(ctx, text, style, size, align, None);
        text.text().cursor = Some(Cursor::default());
        TextEditor(Stack(Offset::Start, Offset::Start, Size::Fit, Size::Fit, Padding::default()), text, TextCursor::new(ctx, style, size))
    }

    pub fn text(&mut self) -> &mut BasicText { self.1.text() }

    pub fn apply_edit(&mut self, _ctx: &mut Context, key: &Key) {
        let index = self.text().cursor.unwrap();
        match key {
            Key::Named(NamedKey::Enter) => {
                match index >= self.text().spans[0].text.len() {
                    true => self.text().spans[0].text.push('\n'),
                    false => self.text().spans[0].text.insert(index, '\n'),
                };
                if let Some(c) = self.text().cursor.as_mut() {*c += 1};
            },
            Key::Named(NamedKey::Space) => {
                match index >= self.text().spans[0].text.len() {
                    true => self.text().spans[0].text.push(' '),
                    false => self.text().spans[0].text.insert(index, ' '),
                };
                if let Some(c) = self.text().cursor.as_mut() {*c += 1};
            },
            Key::Named(NamedKey::Delete | NamedKey::Backspace) => {
                self.text().spans[0].text = {
                    let mut chars: Vec<char> = self.text().spans[0].text.chars().collect();

                    match chars.len() {
                        1 => chars.clear(),
                        _ if index >= chars.len() => {chars.pop();},
                        _ => {chars.remove(index);}
                    }

                    chars.into_iter().collect()
                };
                if let Some(c) = self.text().cursor.as_mut() { *c = c.saturating_sub(1); }
            },
            Key::Character(c) => {
                match index >= self.text().spans[0].text.len() {
                    true => c.chars().next().map(|ch| self.text().spans[0].text.push(ch)),
                    false => c.chars().next().map(|ch| self.text().spans[0].text.insert(index, ch)),
                };
                if let Some(c) = self.text().cursor.as_mut() {*c += 1;}
            },
            _ => {}
        };
    }

    pub fn display_cursor(&mut self, display: bool) {
        self.2.1.display(display)
    }
}

impl OnEvent for TextEditor {
    fn on_event(&mut self, _ctx: &mut Context, event: &mut dyn Event) -> bool {
        if let Some(TickEvent) = event.downcast_ref::<TickEvent>() {
            let cursor_pos = self.text().cursor_position();
            *self.2.x_offset() = Offset::Static(cursor_pos.0);
            *self.2.y_offset() = Offset::Static(cursor_pos.1+2.0);
        } else if let Some(event) = event.downcast_ref::<MouseEvent>() {
            if event.state == MouseState::Pressed && event.position.is_some() {
                self.text().cursor_click(event.position.unwrap().0, event.position.unwrap().1)
            }
        }
        
        true
    }
}

#[derive(Component, Debug)]
pub struct TextCursor(Stack, Opt<Rectangle>);

impl OnEvent for TextCursor {}

impl TextCursor {
    pub fn new(ctx: &mut Context, style: TextStyle, size: f32) -> Self {
        let (color, _) = style.get(ctx);
        TextCursor(
            Stack(Offset::Start, Offset::End, Size::Static(2.0), Size::Static(size), Padding::default()), 
            Opt::new(Rectangle::new(color, 0.0, None), false)
        )
    }

    pub fn x_offset(&mut self) -> &mut Offset { &mut self.0.0 }
    pub fn y_offset(&mut self) -> &mut Offset { &mut self.0.1 }
}

/// # Bulleted Text
///
/// A component for rendering lists with bullet points.
///
/// <img src="https://raw.githubusercontent.com/ramp-stack/pelican_ui_std/main/src/examples/bulleted_text.png"
///      alt="Bulleted Text Example"
///      width="400">
///
/// ## Example
///```rust
/// let text_size = ctx.theme.fonts.size.md;
/// let items = vec!["Feed the chairs at midnight.", "Borrow a broom from the moon.", "Vacuum the car inside out.", "Hide the clock beneath the carpet."];
/// let list = BulletedText::new(ctx, items, TextStyle::Primary, text_size);
///```
#[derive(Debug, Component)]
pub struct BulletedText(Column, Vec<BulletedTextContent>);

impl OnEvent for BulletedText {}

impl BulletedText {
    pub fn new(ctx: &mut Context, text: Vec<&str>, style: TextStyle, size: f32) -> Self {
        let color = style.get(ctx).0;

        let items = text.into_iter().map(|t| BulletedTextContent::new(ctx, t, color, style, size)).collect();
        

        BulletedText(Column::center(8.0), items)
    }
}

#[derive(Debug, Component)]

struct BulletedTextContent(Row, Shape, ExpandableText);
impl OnEvent for BulletedTextContent {}

impl BulletedTextContent {
    fn new(ctx: &mut Context, text: &str, color: Color, style: TextStyle, size: f32) -> Self {
        BulletedTextContent(
            Row::new(size*0.75, Offset::Center, Size::Fit, Padding::default()), // change this offset to be line_height - circle size / 2
            Circle::new(size*0.2, color, false),
            ExpandableText::new(ctx, text, style, size, Align::Left, None)
        )
    }

    // fn text(&mut self) -> &mut BasicText { self.2.text() }
}