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::*;
}
}
+#[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> {
rendered: HashMap::new(),
pos: initial_pos,
last_size: None,
+ ui_mode: UIMode::Normal,
+ selection: None,
};
Ok(ff)
}
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());
}
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>
}
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();
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,
}
}