From: Simon Tatham Date: Thu, 4 Jan 2024 07:45:08 +0000 (+0000) Subject: Implement selecting things in File. X-Git-Url: https://www.chiark.greenend.org.uk/ucgi/~ian/git?a=commitdiff_plain;h=4c5e2a87b91e958f63b87e581789fc40e663d4dd;p=mastodonochrome.git Implement selecting things in File. 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. --- diff --git a/src/file.rs b/src/file.rs index 1042b7c..b39ef27 100644 --- a/src/file.rs +++ b/src/file.rs @@ -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 FileContents { } } +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +enum SelectionPurpose { + ExamineUser, + StatusInfo, +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +enum UIMode { + Normal, + Select(HighlightType, SelectionPurpose), +} + struct File { contents: FileContents, rendered: HashMap>, pos: FilePosition, last_size: Option<(usize, usize)>, + ui_mode: UIMode, + selection: Option<(isize, usize)>, } impl File { @@ -222,6 +237,8 @@ impl File { rendered: HashMap::new(), pos: initial_pos, last_size: None, + ui_mode: UIMode::Normal, + selection: None, }; Ok(ff) } @@ -232,7 +249,21 @@ impl File { 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 File { 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 + { + 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 @@ -503,33 +693,75 @@ impl } 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 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, } }