chiark / gitweb /
Implement selecting things in File.
authorSimon Tatham <anakin@pobox.com>
Thu, 4 Jan 2024 07:45:08 +0000 (07:45 +0000)
committerSimon Tatham <anakin@pobox.com>
Thu, 4 Jan 2024 08:16:16 +0000 (08:16 +0000)
This makes the [E] and [I] keystrokes begin working in general, to
examine a selected user and to view the full info for a selected
status.

src/file.rs

index 1042b7caf13f6d4d81225ff3e30493d8ae4c1c0b..b39ef27cb30de92b11acc063c0c4b1d676db4813 100644 (file)
@@ -1,6 +1,7 @@
 use std::cmp::{min, max};
 use std::collections::{HashMap, HashSet};
 
+use super::activity_stack::UtilityActivity;
 use super::client::{Client, ClientError, FeedId, FeedExtend};
 use super::coloured_string::ColouredString;
 use super::text::*;
@@ -185,11 +186,25 @@ impl<Type: FileType, Source: FileDataSource> FileContents<Type,Source> {
     }
 }
 
+#[derive(Debug, PartialEq, Eq, Clone, Copy)]
+enum SelectionPurpose {
+    ExamineUser,
+    StatusInfo,
+}
+
+#[derive(Debug, PartialEq, Eq, Clone, Copy)]
+enum UIMode {
+    Normal,
+    Select(HighlightType, SelectionPurpose),
+}
+
 struct File<Type: FileType, Source: FileDataSource> {
     contents: FileContents<Type, Source>,
     rendered: HashMap<isize, Vec<ColouredString>>,
     pos: FilePosition,
     last_size: Option<(usize, usize)>,
+    ui_mode: UIMode,
+    selection: Option<(isize, usize)>,
 }
 
 impl<Type: FileType, Source: FileDataSource> File<Type, Source> {
@@ -222,6 +237,8 @@ impl<Type: FileType, Source: FileDataSource> File<Type, Source> {
             rendered: HashMap::new(),
             pos: initial_pos,
             last_size: None,
+            ui_mode: UIMode::Normal,
+            selection: None,
         };
         Ok(ff)
     }
@@ -232,7 +249,21 @@ impl<Type: FileType, Source: FileDataSource> File<Type, Source> {
         if !self.rendered.contains_key(&index) {
             let mut lines = Vec::new();
 
-            for line in self.contents.get(index).render(w) {
+            let highlight = match self.ui_mode {
+                UIMode::Normal => None,
+                UIMode::Select(htype, _purpose) => match self.selection {
+                    None => None,
+                    Some((item, sub)) => if item == index {
+                        Some(Highlight(htype, sub))
+                    } else {
+                        None
+                    }
+                }
+            };
+
+            for line in self.contents.get(index)
+                .render_highlighted(w, highlight)
+            {
                 for frag in line.split(w) {
                     lines.push(frag.to_owned());
                 }
@@ -444,6 +475,165 @@ impl<Type: FileType, Source: FileDataSource> File<Type, Source> {
         self.ensure_enough_rendered();
         action
     }
+
+    fn last_selectable_above(&self, htype: HighlightType, index: isize) ->
+        Option<(isize, usize)>
+    {
+        for i in (self.contents.origin..=index).rev() {
+            let n = self.contents.get(i).count_highlightables(htype);
+            if n > 0 {
+                return Some((i, n-1));
+            }
+        }
+
+        None
+    }
+
+    fn first_selectable_below(&self, htype: HighlightType, index: isize) ->
+        Option<(isize, usize)>
+    {
+        for i in index..self.contents.index_limit() {
+            let n = self.contents.get(i).count_highlightables(htype);
+            if n > 0 {
+                return Some((i, 0));
+            }
+        }
+
+        None
+    }
+
+    fn rerender_selected_item(&mut self) {
+        if let Some((index, _)) = self.selection {
+            self.rendered.remove(&index);
+        }
+    }
+
+    fn start_selection(&mut self, htype: HighlightType,
+                       purpose: SelectionPurpose) -> LogicalAction
+    {
+        let item = self.pos.item();
+        let selection =
+            self.last_selectable_above(htype, item).or_else(
+                || self.first_selectable_below(htype, item + 1));
+
+        if selection.is_some() {
+            self.ui_mode = UIMode::Select(htype, purpose);
+            self.change_selection_to(selection, false);
+            LogicalAction::Nothing
+        } else {
+            LogicalAction::Beep
+        }
+    }
+
+    fn change_selection_to(&mut self, new_selection: Option<(isize, usize)>,
+                           none_ok: bool) -> LogicalAction
+    {
+        let result = if new_selection.is_some() {
+            self.rerender_selected_item();
+            self.selection = new_selection;
+            self.rerender_selected_item();
+            self.ensure_enough_rendered();
+            LogicalAction::Nothing
+        } else {
+            if none_ok {
+                LogicalAction::Nothing
+            } else {
+                LogicalAction::Beep
+            }
+        };
+
+        result
+    }
+
+    fn selection_up(&mut self) -> LogicalAction {
+        let htype = match self.ui_mode {
+            UIMode::Select(htype, _purpose) => htype,
+            _ => return LogicalAction::Beep,
+        };
+
+        let new_selection = match self.selection {
+            None => None,
+            Some((item, sub)) => if sub > 0 {
+                Some((item, sub - 1))
+            } else {
+                self.last_selectable_above(htype, item - 1)
+            }
+        };
+
+        self.change_selection_to(new_selection, false)
+    }
+
+    fn selection_down(&mut self) -> LogicalAction {
+        let htype = match self.ui_mode {
+            UIMode::Select(htype, _purpose) => htype,
+            _ => return LogicalAction::Beep,
+        };
+
+        let new_selection = match self.selection {
+            None => None,
+            Some((item, sub)) => {
+                let count = self.contents.get(item).count_highlightables(htype);
+                if sub + 1 < count {
+                    Some((item, sub + 1))
+                } else {
+                    self.first_selectable_below(htype, item + 1)
+                }
+            }
+        };
+
+        self.change_selection_to(new_selection, false)
+    }
+
+    fn end_selection(&mut self) {
+        if self.selection.is_some() {
+            self.rerender_selected_item();
+            self.selection = None;
+            self.ensure_enough_rendered();
+        }
+        self.ui_mode = UIMode::Normal;
+    }
+
+    fn abort_selection(&mut self) -> LogicalAction {
+        self.end_selection();
+        LogicalAction::Nothing
+    }
+
+    fn selected_id(&self, selection: Option<(isize, usize)>)
+                   -> Option<String>
+    {
+        let htype = match self.ui_mode {
+            UIMode::Select(htype, _purpose) => htype,
+            _ => return None,
+        };
+
+        match selection {
+            Some((item, sub)) => self.contents.get(item)
+                .highlighted_id(Some(Highlight(htype, sub))),
+            None => None,
+        }
+    }
+
+    fn complete_selection(&mut self) -> LogicalAction {
+        let (_htype, purpose) = match self.ui_mode {
+            UIMode::Select(htype, purpose) => (htype, purpose),
+            _ => return LogicalAction::Beep,
+        };
+
+        let result = if let Some(id) = self.selected_id(self.selection) {
+            match purpose {
+                SelectionPurpose::ExamineUser => LogicalAction::Goto(
+                    UtilityActivity::ExamineUser(id).into()),
+                SelectionPurpose::StatusInfo => LogicalAction::Goto(
+                    UtilityActivity::InfoStatus(id).into()),
+            }
+        } else {
+            LogicalAction::Beep
+        };
+
+        self.end_selection();
+
+        result
+    }
 }
 
 impl<Type: FileType, Source: FileDataSource>
@@ -503,33 +693,75 @@ impl<Type: FileType, Source: FileDataSource>
         }
 
         let fs = FileStatusLine::new();
-        let fs = if at_bottom {
-            fs.add(Pr('-'), "Up", 99)
-        } else {
-            fs.add(Space, "Down", 99)
-        };
-        let fs = fs.add(Pr('q'), "Exit", 100);
-        // FIXME: document more keys
-
-        // We calculate the percentage through the file in a loose
-        // sort of way, by assuming all items are the same size, and
-        // only calculating a partial item for the one we're actually
-        // in the middle of. This isn't how Mono did it, but Mono
-        // didn't have to dynamically rewrap whenever the window
-        // resized.
-        //
-        // (A robust way to get precise line-based percentages even so
-        // would be to eagerly rewrap the entire collection of items
-        // on every resize, but I don't think that's a sensible
-        // tradeoff!)
-        let fs = {
-            let base = self.contents.first_index();
-            let full_items = (start_item - base) as usize;
-            let total_items = (self.contents.index_limit() - base) as usize;
-            let mult = self.rendered.get(&start_item).unwrap().len();
-            fs.set_proportion(
-                full_items * mult + start_line,
-                total_items * mult)
+        let fs = match self.ui_mode {
+            UIMode::Normal => {
+                let fs = if at_bottom {
+                    fs.add(Pr('-'), "Up", 99)
+                } else {
+                    fs.add(Space, "Down", 99)
+                };
+                let fs = if Type::Item::can_highlight(
+                    HighlightType::WholeStatus)
+                {
+                    fs.add(Pr('s'), "Reply", 42)
+                } else {
+                    fs
+                };
+                let fs = if Type::Item::can_highlight(HighlightType::User) {
+                    fs.add(Pr('e'), "Examine", 40)
+                } else {
+                    fs
+                };
+                let fs = if Type::Item::can_highlight(HighlightType::Status) {
+                    fs.add(Pr('i'), "Post Info", 38)
+                } else {
+                    fs
+                };
+                let fs = if Type::Item::can_highlight(HighlightType::Status) {
+                    fs.add(Pr('t'), "Thread", 35)
+                } else {
+                    fs
+                };
+                let fs = if Type::Item::can_highlight(
+                    HighlightType::WholeStatus)
+                {
+                    fs.add(Pr('f'), "Fave", 41)
+                        .add(Ctrl('B'), "Boost", 41)
+                } else {
+                    fs
+                };
+                let fs = fs.add(Pr('q'), "Exit", 100);
+
+                // We calculate the percentage through the file in a
+                // loose sort of way, by assuming all items are the
+                // same size, and only calculating a partial item for
+                // the one we're actually in the middle of. This isn't
+                // how Mono did it, but Mono didn't have to
+                // dynamically rewrap whenever the window resized.
+                //
+                // (A robust way to get precise line-based percentages
+                // even so would be to eagerly rewrap the entire
+                // collection of items on every resize, but I don't
+                // think that's a sensible tradeoff!)
+                let base = self.contents.first_index();
+                let full_items = (start_item - base) as usize;
+                let total_items = (self.contents.index_limit() - base) as usize;
+                let mult = self.rendered.get(&start_item).unwrap().len();
+                fs.set_proportion(
+                    full_items * mult + start_line,
+                    total_items * mult)
+            }
+            UIMode::Select(_htype, purpose) => {
+                let fs = match purpose {
+                    SelectionPurpose::ExamineUser =>
+                        fs.add(Space, "Examine", 98),
+                    SelectionPurpose::StatusInfo =>
+                        fs.add(Space, "Info", 98),
+                };
+                fs.add(Pr('+'), "Down", 99)
+                    .add(Pr('-'), "Up", 99)
+                    .add(Pr('Q'), "Quit", 100)
+            }
         };
 
         let fs = fs.finalise();
@@ -547,49 +779,77 @@ impl<Type: FileType, Source: FileDataSource>
             None => panic!("handle_keypress before resize"),
         };
 
-        match key {
-            Pr('q') | Pr('Q') => LogicalAction::Pop,
-            Up => {
-                self.move_up(1);
-                LogicalAction::Nothing
-            }
-            Pr('-') | Pr('b') | Pr('B') | PgUp | Left => {
-                self.move_up(max(1, h - min(h, 3)));
-                LogicalAction::Nothing
-            }
-            Down => {
-                self.move_down(1);
-                LogicalAction::Nothing
-            }
-            Return => {
-                let oldpos = self.pos;
-                self.move_down(1);
-                if oldpos == self.pos {
-                    LogicalAction::Pop
-                } else {
+        match self.ui_mode {
+            UIMode::Normal => match key {
+                Pr('q') | Pr('Q') => LogicalAction::Pop,
+                Up => {
+                    self.move_up(1);
                     LogicalAction::Nothing
                 }
-            }
-            Space | PgDn | Right => {
-                self.move_down(max(1, h - min(h, 3)));
-                LogicalAction::Nothing
-            }
-            Pr('0') | Home => {
-                if self.at_top() && self.contents.extender.is_some() {
-                    match self.try_extend(client) {
-                        Ok(_) => LogicalAction::Nothing,
-                        Err(e) => LogicalAction::Error(e),
+                Pr('-') | Pr('b') | Pr('B') | PgUp | Left => {
+                    self.move_up(max(1, h - min(h, 3)));
+                    LogicalAction::Nothing
+                }
+                Down => {
+                    self.move_down(1);
+                    LogicalAction::Nothing
+                }
+                Return => {
+                    let oldpos = self.pos;
+                    self.move_down(1);
+                    if oldpos == self.pos {
+                        LogicalAction::Pop
+                    } else {
+                        LogicalAction::Nothing
                     }
-                } else {
-                    self.goto_top();
+                }
+                Space | PgDn | Right => {
+                    self.move_down(max(1, h - min(h, 3)));
                     LogicalAction::Nothing
                 }
+                Pr('0') | Home => {
+                    if self.at_top() && self.contents.extender.is_some() {
+                        match self.try_extend(client) {
+                            Ok(_) => LogicalAction::Nothing,
+                            Err(e) => LogicalAction::Error(e),
+                        }
+                    } else {
+                        self.goto_top();
+                        LogicalAction::Nothing
+                    }
+                }
+                Pr('z') | Pr('Z') | End => {
+                    self.goto_bottom();
+                    LogicalAction::Nothing
+                }
+
+                Pr('e') | Pr('E') => {
+                    if Type::Item::can_highlight(HighlightType::User) {
+                        self.start_selection(HighlightType::User,
+                                             SelectionPurpose::ExamineUser)
+                    } else {
+                        LogicalAction::Nothing
+                    }
+                }
+
+                Pr('i') | Pr('I') => {
+                    if Type::Item::can_highlight(HighlightType::Status) {
+                        self.start_selection(HighlightType::Status,
+                                             SelectionPurpose::StatusInfo)
+                    } else {
+                        LogicalAction::Nothing
+                    }
+                }
+
+                _ => LogicalAction::Nothing,
             }
-            Pr('z') | Pr('Z') | End => {
-                self.goto_bottom();
-                LogicalAction::Nothing
+            UIMode::Select(..) => match key {
+                Space => self.complete_selection(),
+                Pr('-') | Up => self.selection_up(),
+                Pr('+') | Down => self.selection_down(),
+                Pr('q') | Pr('Q') => self.abort_selection(),
+                _ => LogicalAction::Nothing,
             }
-            _ => LogicalAction::Nothing,
         }
     }