sauva 0.1.0

Terminal Unicode Explorer
use crate::{
    unicode::{CodePoint, Plane, plane::PlaneRange},
    viewport::ListViewport,
};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BrowseLevel {
    Plane,
    Range,
    CodePointTable,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BrowseMove {
    Up,
    Down,
    Left,
    Right,
    LargeBackward,
    LargeForward,
    First,
    Last,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BrowseTarget {
    Browser,
    Inspector,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BrowseState {
    level: BrowseLevel,
    cursor: CodePoint,
    plane_viewport: ListViewport,
    range_viewport: ListViewport,
    table_viewport: ListViewport,
}

impl BrowseState {
    pub const fn at(level: BrowseLevel, cursor: CodePoint) -> Self {
        Self {
            level,
            cursor,
            plane_viewport: ListViewport::new(),
            range_viewport: ListViewport::new(),
            table_viewport: ListViewport::new(),
        }
    }

    pub const fn level(self) -> BrowseLevel {
        self.level
    }

    pub const fn cursor(self) -> CodePoint {
        self.cursor
    }

    pub fn resize_list_viewports(&mut self, height: usize) {
        self.plane_viewport.resize(
            height,
            usize::from(Plane::for_code_point(self.cursor).number()),
            Plane::COUNT,
        );
        self.range_viewport.resize(
            height,
            usize::from(PlaneRange::for_code_point(self.cursor).index()),
            PlaneRange::COUNT_PER_PLANE,
        );
        self.table_viewport.resize(
            height.saturating_sub(1),
            usize::from(table_row(self.cursor)),
            16,
        );
    }

    pub fn visible_list_items(&self) -> Option<std::ops::Range<usize>> {
        match self.level {
            BrowseLevel::Plane => Some(self.plane_viewport.visible_range(Plane::COUNT)),
            BrowseLevel::Range => Some(
                self.range_viewport
                    .visible_range(PlaneRange::COUNT_PER_PLANE),
            ),
            BrowseLevel::CodePointTable => None,
        }
    }

    pub fn visible_table_rows(&self) -> Option<std::ops::Range<usize>> {
        (self.level == BrowseLevel::CodePointTable).then(|| self.table_viewport.visible_range(16))
    }

    pub fn advance(&mut self) -> BrowseTarget {
        match self.level {
            BrowseLevel::Plane => {
                self.level = BrowseLevel::Range;
                self.ensure_current_item_visible();
            }
            BrowseLevel::Range => {
                self.level = BrowseLevel::CodePointTable;
                self.ensure_current_item_visible();
            }
            BrowseLevel::CodePointTable => return BrowseTarget::Inspector,
        }
        BrowseTarget::Browser
    }

    pub fn back(&mut self) {
        match self.level {
            BrowseLevel::Plane => {}
            BrowseLevel::Range => {
                self.level = BrowseLevel::Plane;
                self.ensure_current_item_visible();
            }
            BrowseLevel::CodePointTable => {
                self.level = BrowseLevel::Range;
                self.ensure_current_item_visible();
            }
        }
    }

    pub fn move_cursor(&mut self, movement: BrowseMove) -> bool {
        let previous = self.cursor;
        self.cursor = match self.level {
            BrowseLevel::Plane => move_plane(self.cursor, movement),
            BrowseLevel::Range => move_range(self.cursor, movement),
            BrowseLevel::CodePointTable => move_table(self.cursor, movement),
        };
        self.ensure_current_item_visible();

        debug_assert!(PlaneRange::for_code_point(self.cursor).contains(self.cursor));
        self.cursor != previous
    }

    fn ensure_current_item_visible(&mut self) {
        match self.level {
            BrowseLevel::Plane => self.plane_viewport.ensure_visible(
                usize::from(Plane::for_code_point(self.cursor).number()),
                Plane::COUNT,
            ),
            BrowseLevel::Range => self.range_viewport.ensure_visible(
                usize::from(PlaneRange::for_code_point(self.cursor).index()),
                PlaneRange::COUNT_PER_PLANE,
            ),
            BrowseLevel::CodePointTable => {
                self.table_viewport
                    .ensure_visible(usize::from(table_row(self.cursor)), 16);
            }
        }
    }
}

fn table_row(cursor: CodePoint) -> u8 {
    (cursor.value() & 0xff) as u8 / 16
}

fn move_plane(cursor: CodePoint, movement: BrowseMove) -> CodePoint {
    let plane = Plane::for_code_point(cursor);
    let target = match movement {
        BrowseMove::Up => plane.number().checked_sub(1),
        BrowseMove::Down => plane.number().checked_add(1),
        BrowseMove::First => Some(0),
        BrowseMove::Last => Some(Plane::LAST_NUMBER),
        _ => None,
    };

    target.and_then(Plane::new).map_or(cursor, |target| {
        valid_code_point(target.start().value() | (cursor.value() & 0xffff))
    })
}

fn move_range(cursor: CodePoint, movement: BrowseMove) -> CodePoint {
    let range = PlaneRange::for_code_point(cursor);
    let target = match movement {
        BrowseMove::Up => Some(range.index().saturating_sub(1)),
        BrowseMove::Down => Some(range.index().saturating_add(1)),
        BrowseMove::LargeBackward => Some(range.index().saturating_sub(16)),
        BrowseMove::LargeForward => Some(range.index().saturating_add(16)),
        BrowseMove::First => Some(0),
        BrowseMove::Last => Some(u8::MAX),
        _ => None,
    };

    let offset = range
        .offset_of(cursor)
        .expect("a code point belongs to its derived range");
    target.map_or(cursor, |index| {
        range.plane().range(index).code_point(offset)
    })
}

fn move_table(cursor: CodePoint, movement: BrowseMove) -> CodePoint {
    let plane = Plane::for_code_point(cursor);
    let range = PlaneRange::for_code_point(cursor);
    let value = cursor.value();
    let target = match movement {
        BrowseMove::Left if value > plane.start().value() => Some(value - 1),
        BrowseMove::Right if value < plane.end().value() => Some(value + 1),
        BrowseMove::Up if value - plane.start().value() >= 16 => Some(value - 16),
        BrowseMove::Down if plane.end().value() - value >= 16 => Some(value + 16),
        BrowseMove::LargeBackward => range.index().checked_sub(1).map(|index| {
            range
                .plane()
                .range(index)
                .code_point(range_offset(range, cursor))
                .value()
        }),
        BrowseMove::LargeForward => range.index().checked_add(1).map(|index| {
            range
                .plane()
                .range(index)
                .code_point(range_offset(range, cursor))
                .value()
        }),
        BrowseMove::First => Some(range.start().value()),
        BrowseMove::Last => Some(range.end().value()),
        _ => None,
    };

    target.map_or(cursor, valid_code_point)
}

fn range_offset(range: PlaneRange, cursor: CodePoint) -> u8 {
    range
        .offset_of(cursor)
        .expect("a code point belongs to its derived range")
}

fn valid_code_point(value: u32) -> CodePoint {
    CodePoint::new(value).expect("browser-derived values are valid Unicode code points")
}

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

    #[test]
    fn advances_and_goes_back_through_the_browse_hierarchy() {
        let mut state = browse_state(0x0041);

        assert_eq!(state.level(), BrowseLevel::Plane);
        assert_eq!(state.advance(), BrowseTarget::Browser);
        assert_eq!(state.level(), BrowseLevel::Range);
        assert_eq!(state.advance(), BrowseTarget::Browser);
        assert_eq!(state.level(), BrowseLevel::CodePointTable);
        assert_eq!(state.advance(), BrowseTarget::Inspector);
        assert_eq!(state.level(), BrowseLevel::CodePointTable);
        state.back();
        assert_eq!(state.level(), BrowseLevel::Range);
        state.back();
        assert_eq!(state.level(), BrowseLevel::Plane);
        state.back();
        assert_eq!(state.level(), BrowseLevel::Plane);
    }

    #[test]
    fn moves_between_planes_while_preserving_the_plane_offset() {
        let mut state = browse_state(0x2_abcd);

        assert!(state.move_cursor(BrowseMove::Up));
        assert_eq!(state.cursor().value(), 0x1_abcd);
        assert!(state.move_cursor(BrowseMove::First));
        assert_eq!(state.cursor().value(), 0xabcd);
        assert!(!state.move_cursor(BrowseMove::Up));
        assert!(state.move_cursor(BrowseMove::Last));
        assert_eq!(state.cursor().value(), 0x10_abcd);
        assert!(!state.move_cursor(BrowseMove::Down));
    }

    #[test]
    fn moves_between_ranges_while_preserving_the_range_offset() {
        let mut state = browse_state_at(BrowseLevel::Range, 0x1_23ab);

        assert!(state.move_cursor(BrowseMove::Up));
        assert_eq!(state.cursor().value(), 0x1_22ab);
        assert!(state.move_cursor(BrowseMove::LargeBackward));
        assert_eq!(state.cursor().value(), 0x1_12ab);
        assert!(state.move_cursor(BrowseMove::First));
        assert_eq!(state.cursor().value(), 0x1_00ab);
        assert!(!state.move_cursor(BrowseMove::Up));
        assert!(state.move_cursor(BrowseMove::Last));
        assert_eq!(state.cursor().value(), 0x1_ffab);
        assert!(!state.move_cursor(BrowseMove::LargeForward));

        let mut near_first = browse_state_at(BrowseLevel::Range, 0x1_05ab);
        assert!(near_first.move_cursor(BrowseMove::LargeBackward));
        assert_eq!(near_first.cursor().value(), 0x1_00ab);
        let mut near_last = browse_state_at(BrowseLevel::Range, 0x1_faab);
        assert!(near_last.move_cursor(BrowseMove::LargeForward));
        assert_eq!(near_last.cursor().value(), 0x1_ffab);
    }

    #[test]
    fn table_directional_movement_crosses_ranges_but_not_planes() {
        let mut state = browse_state_at(BrowseLevel::CodePointTable, 0x00ff);

        assert!(state.move_cursor(BrowseMove::Right));
        assert_eq!(state.cursor().value(), 0x0100);
        assert!(state.move_cursor(BrowseMove::Up));
        assert_eq!(state.cursor().value(), 0x00f0);

        let mut first = browse_state_at(BrowseLevel::CodePointTable, 0x0000);
        assert!(!first.move_cursor(BrowseMove::Left));
        assert!(!first.move_cursor(BrowseMove::Up));

        let mut last = browse_state_at(BrowseLevel::CodePointTable, 0x10_ffff);
        assert!(!last.move_cursor(BrowseMove::Right));
        assert!(!last.move_cursor(BrowseMove::Down));
    }

    #[test]
    fn table_large_movement_preserves_offset_and_stops_at_plane_boundaries() {
        let mut state = browse_state_at(BrowseLevel::CodePointTable, 0x1_23ab);

        assert!(state.move_cursor(BrowseMove::LargeBackward));
        assert_eq!(state.cursor().value(), 0x1_22ab);
        assert!(state.move_cursor(BrowseMove::LargeForward));
        assert_eq!(state.cursor().value(), 0x1_23ab);
        assert!(state.move_cursor(BrowseMove::First));
        assert_eq!(state.cursor().value(), 0x1_2300);
        assert!(state.move_cursor(BrowseMove::Last));
        assert_eq!(state.cursor().value(), 0x1_23ff);

        let mut first = browse_state_at(BrowseLevel::CodePointTable, 0x1_00ab);
        assert!(!first.move_cursor(BrowseMove::LargeBackward));
        let mut last = browse_state_at(BrowseLevel::CodePointTable, 0x1_ffab);
        assert!(!last.move_cursor(BrowseMove::LargeForward));
    }

    #[test]
    fn list_viewports_scroll_at_edges_and_are_retained_per_level() {
        let mut state = browse_state(0x0041);
        state.resize_list_viewports(10);

        for _ in 0..10 {
            state.move_cursor(BrowseMove::Down);
        }
        assert_eq!(state.visible_list_items(), Some(1..11));
        state.move_cursor(BrowseMove::Up);
        assert_eq!(state.visible_list_items(), Some(1..11));

        state.advance();
        assert_eq!(state.visible_list_items(), Some(0..10));

        for _ in 0..10 {
            state.move_cursor(BrowseMove::Down);
        }
        assert_eq!(state.visible_list_items(), Some(1..11));

        state.move_cursor(BrowseMove::Up);
        assert_eq!(state.visible_list_items(), Some(1..11));
        state.back();
        assert_eq!(state.visible_list_items(), Some(1..11));
        state.advance();
        assert_eq!(state.visible_list_items(), Some(1..11));
    }

    #[test]
    fn table_viewport_keeps_the_column_header_and_scrolls_only_at_row_edges() {
        let mut state = browse_state_at(BrowseLevel::CodePointTable, 0x0041);
        state.resize_list_viewports(10);

        assert_eq!(state.visible_table_rows(), Some(0..9));
        for _ in 0..5 {
            state.move_cursor(BrowseMove::Down);
        }
        assert_eq!(state.cursor().value(), 0x0091);
        assert_eq!(state.visible_table_rows(), Some(1..10));

        state.move_cursor(BrowseMove::Up);
        assert_eq!(state.cursor().value(), 0x0081);
        assert_eq!(state.visible_table_rows(), Some(1..10));

        state.move_cursor(BrowseMove::Last);
        assert_eq!(state.visible_table_rows(), Some(7..16));
        state.move_cursor(BrowseMove::Right);
        assert_eq!(state.cursor().value(), 0x0100);
        assert_eq!(state.visible_table_rows(), Some(0..9));
    }

    fn browse_state(value: u32) -> BrowseState {
        BrowseState::at(BrowseLevel::Plane, CodePoint::new(value).unwrap())
    }

    fn browse_state_at(level: BrowseLevel, value: u32) -> BrowseState {
        let mut state = browse_state(value);
        state.level = level;
        state
    }
}