piki-gui 0.7.1

FLTK-based GUI for Piki personal wiki application
Documentation
#![allow(dead_code)]

use fltk::{prelude::*, *};

/// Helper function to create a brighter version of a color
/// Increases each RGB component by a factor (clamped to 255)
fn brighten_color(color: enums::Color, factor: f32) -> enums::Color {
    let (r, g, b) = color.to_rgb();
    let new_r = ((r as f32 * factor).min(255.0)) as u8;
    let new_g = ((g as f32 * factor).min(255.0)) as u8;
    let new_b = ((b as f32 * factor).min(255.0)) as u8;
    enums::Color::from_rgb(new_r, new_g, new_b)
}

/// What the left half of the status bar shows: the note we are on, or — while a
/// link is hovered (by the mouse or by the caret sitting inside it) — that
/// link's destination.
///
/// Both halves are kept as data instead of being read back off the widget
/// label, because the two are updated by independent event sources: a link
/// click navigates (new note name) while the hover that started the click is
/// still active. Snapshotting the label at hover start and restoring it at
/// hover end would put the *previous* note's name back on screen.
#[derive(Debug, Default, Clone, PartialEq, Eq)]
struct NoteLabel {
    /// Name of the note currently on screen (already formatted for display).
    note: String,
    /// Destination of the hovered link, if any; takes precedence while set.
    hover: Option<String>,
}

impl NoteLabel {
    /// Navigation is authoritative: it sets the new note name and drops any
    /// hover, which belongs to the content that was just replaced.
    fn set_note(&mut self, note: &str) {
        self.note = note.to_string();
        self.hover = None;
    }

    fn set_hover(&mut self, target: Option<&str>) {
        self.hover = target.map(str::to_string);
    }

    fn displayed(&self) -> &str {
        self.hover.as_deref().unwrap_or(&self.note)
    }
}

/// Custom status bar widget that manages two child widgets (note status and save status)
/// and automatically handles layout and rendering
pub struct StatusBar {
    // Background frame
    background: frame::Frame,
    // Left side: note status (button for clicking)
    note_status: button::Button,
    // Right side: save status (frame for display)
    save_status: frame::Frame,
    // Current note name plus any transient link-hover destination
    note_label: NoteLabel,
    // Colors
    bg_color: enums::Color,
    text_color: enums::Color,
    hover_color: enums::Color,
}

impl StatusBar {
    /// Create a new StatusBar widget
    ///
    /// # Arguments
    /// * `x` - X position
    /// * `y` - Y position
    /// * `w` - Width
    /// * `h` - Height
    pub fn new(x: i32, y: i32, w: i32, h: i32) -> Self {
        let bg_color = enums::Color::from_rgb(136, 167, 246); // Default blue
        let text_color = enums::Color::White;
        let hover_color = brighten_color(bg_color, 1.2); // 20% brighter

        // Create background frame
        let mut background = frame::Frame::new(x, y, w, h, None);
        background.set_frame(enums::FrameType::FlatBox);
        background.set_color(bg_color);

        // Create note status button (left side)
        let mut note_status = button::Button::new(x + 5, y, w / 2 - 10, h, None);
        note_status.set_frame(enums::FrameType::FlatBox);
        note_status.set_align(enums::Align::Left | enums::Align::Inside);
        note_status.set_label_size(app::font_size() - 1);
        note_status.set_color(bg_color);
        note_status.set_label_color(text_color);

        // Add hover effect for note status
        let mut but2 = note_status.clone();
        let hover_bg = hover_color;
        note_status.handle(move |_, evt| match evt {
            enums::Event::Enter => {
                but2.set_color(hover_bg);
                but2.redraw();
                true
            }
            enums::Event::Leave => {
                but2.set_color(bg_color);
                but2.redraw();
                true
            }
            _ => false,
        });

        // Create save status frame (right side)
        let mut save_status = frame::Frame::new(x + 5 + w / 2, y, w / 2 - 10, h, None);
        save_status.set_frame(enums::FrameType::FlatBox);
        save_status.set_align(enums::Align::Right | enums::Align::Inside);
        save_status.set_label_size(app::font_size() - 1);
        save_status.set_color(bg_color);
        save_status.set_label_color(text_color);

        StatusBar {
            background,
            note_status,
            save_status,
            note_label: NoteLabel::default(),
            bg_color,
            text_color,
            hover_color,
        }
    }

    /// Set the background color of the status bar
    /// Also automatically updates the hover color to be a brighter version
    pub fn set_color(&mut self, color: enums::Color) {
        self.bg_color = color;
        self.hover_color = brighten_color(color, 1.2); // 20% brighter
        self.background.set_color(color);
        self.note_status.set_color(color);
        self.save_status.set_color(color);

        // Update the hover handler with the new colors
        let mut but2 = self.note_status.clone();
        let bg = color;
        let hover_bg = self.hover_color;
        self.note_status.handle(move |_, evt| match evt {
            enums::Event::Enter => {
                but2.set_color(hover_bg);
                but2.redraw();
                true
            }
            enums::Event::Leave => {
                but2.set_color(bg);
                but2.redraw();
                true
            }
            _ => false,
        });
    }

    /// Set the text color of the status bar
    pub fn set_text_color(&mut self, color: enums::Color) {
        self.text_color = color;
        self.note_status.set_label_color(color);
        self.save_status.set_label_color(color);
    }

    /// Set the note status text (left side).
    ///
    /// This is the authoritative "which note is on screen" label, so it also
    /// drops any link-hover destination currently being shown: navigation
    /// replaces the content under the mouse, making that hover stale.
    pub fn set_note(&mut self, text: &str) {
        self.note_label.set_note(text);
        self.refresh_note_label();
    }

    /// Show the destination of the hovered link in place of the note name, or
    /// pass `None` when the hover ends to fall back to the note on screen.
    pub fn set_link_hover(&mut self, target: Option<&str>) {
        self.note_label.set_hover(target);
        self.refresh_note_label();
    }

    fn refresh_note_label(&mut self) {
        let label = self.note_label.displayed().to_string();
        self.note_status.set_label(&label);
    }

    /// Set the save status text (right side)
    pub fn set_status(&mut self, text: &str) {
        self.save_status.set_label(text);
    }

    /// Set the tooltip for the note status (left side)
    pub fn set_note_tooltip(&mut self, tooltip: &str) {
        self.note_status.set_tooltip(tooltip);
    }

    /// Set the tooltip for the save status (right side)
    pub fn set_status_tooltip(&mut self, tooltip: &str) {
        self.save_status.set_tooltip(tooltip);
    }

    /// Set the hover color for the note status button
    pub fn set_hover_color(&mut self, color: enums::Color) {
        self.hover_color = color;

        // Update the hover handler with the new hover color
        let mut but2 = self.note_status.clone();
        let bg = self.bg_color;
        let hover_bg = color;
        self.note_status.handle(move |_, evt| match evt {
            enums::Event::Enter => {
                but2.set_color(hover_bg);
                but2.redraw();
                true
            }
            enums::Event::Leave => {
                but2.set_color(bg);
                but2.redraw();
                true
            }
            _ => false,
        });
    }

    /// Register a callback for when the note status is clicked
    pub fn on_note_click<F: FnMut(&mut button::Button) + 'static>(&mut self, cb: F) {
        self.note_status.set_callback(cb);
    }

    /// Register a callback for when the save status is clicked
    /// Note: This converts the frame to a button if needed for click handling
    pub fn on_status_click<F: FnMut() + 'static>(&mut self, mut cb: F) {
        // For now, we handle this via a manual event handler
        // since save_status is a Frame, not a Button
        self.save_status.handle(move |_, evt| {
            if evt == enums::Event::Push {
                cb();
                true
            } else {
                false
            }
        });
    }

    /// Get a reference to the note status widget (for external manipulation)
    pub fn note_status_widget(&self) -> button::Button {
        self.note_status.clone()
    }

    /// Get a reference to the save status widget (for external manipulation)
    pub fn save_status_widget(&self) -> frame::Frame {
        self.save_status.clone()
    }

    /// Resize the status bar and update child positions
    pub fn resize(&mut self, x: i32, y: i32, w: i32, h: i32) {
        self.background.resize(x, y, w, h);
        self.note_status.resize(x + 5, y, w / 2 - 10, h);
        self.save_status.resize(x + 5 + w / 2, y, w / 2 - 10, h);
    }

    /// Get the height of the status bar
    pub fn height(&self) -> i32 {
        self.background.height()
    }

    /// Get the width of the status bar
    pub fn width(&self) -> i32 {
        self.background.width()
    }

    /// Get the x position of the status bar
    pub fn x(&self) -> i32 {
        self.background.x()
    }

    /// Get the y position of the status bar
    pub fn y(&self) -> i32 {
        self.background.y()
    }

    /// Hide the status bar
    pub fn hide(&mut self) {
        self.background.hide();
        self.note_status.hide();
        self.save_status.hide();
    }

    /// Show the status bar
    pub fn show(&mut self) {
        self.background.show();
        self.note_status.show();
        self.save_status.show();
    }

    /// Check if the status bar is visible
    pub fn visible(&self) -> bool {
        self.background.visible()
    }
}

#[cfg(test)]
mod tests {
    use super::NoteLabel;

    #[test]
    fn hover_overlays_the_note_name_and_falls_back_to_it() {
        let mut label = NoteLabel::default();
        label.set_note("Note: frontpage");
        assert_eq!(label.displayed(), "Note: frontpage");

        label.set_hover(Some("recipes"));
        assert_eq!(label.displayed(), "recipes");

        label.set_hover(None);
        assert_eq!(label.displayed(), "Note: frontpage");
    }

    #[test]
    fn navigating_while_hovering_leaves_no_stale_note_name() {
        let mut label = NoteLabel::default();
        label.set_note("Note: frontpage");

        // Hover the "recipes" link, then click it: the load sets the new note
        // name while the hover that started the click is still in effect.
        label.set_hover(Some("recipes"));
        label.set_note("Note: recipes");
        assert_eq!(label.displayed(), "Note: recipes");

        // The hover-end event arriving afterwards must not resurrect the note
        // we came from.
        label.set_hover(None);
        assert_eq!(label.displayed(), "Note: recipes");
    }

    #[test]
    fn hover_end_after_back_navigation_keeps_the_note_we_went_back_to() {
        let mut label = NoteLabel::default();
        label.set_note("Note: frontpage");
        label.set_hover(Some("recipes"));
        label.set_note("Note: recipes");

        // Back to the frontpage with the caret landing next to one of its
        // links, then away from it again.
        label.set_note("Note: frontpage");
        label.set_hover(Some("notes"));
        assert_eq!(label.displayed(), "notes");
        label.set_hover(None);
        assert_eq!(label.displayed(), "Note: frontpage");
    }
}