chiark / gitweb /
More or less principled file-position tracking.
authorSimon Tatham <anakin@pobox.com>
Fri, 29 Dec 2023 16:59:20 +0000 (16:59 +0000)
committerSimon Tatham <anakin@pobox.com>
Fri, 29 Dec 2023 18:17:34 +0000 (18:17 +0000)
We still can't actually _scroll_ through the file, because I haven't
made any actual keystrokes work yet. But we begin displaying it at the
bottom, and do something sensible on resizing.

src/file.rs
src/tui.rs

index da20f717bcc6e77a5a84b52f094752a246e12da5..9360ccf184f1a9ca88813da0fd2f409734412bea 100644 (file)
@@ -1,3 +1,5 @@
+use std::collections::HashMap;
+
 use super::client::{Client, ClientError, FeedId, FeedExtend};
 use super::coloured_string::ColouredString;
 use super::text::*;
@@ -6,55 +8,179 @@ use super::tui::{
     OurKey,
 };
 
-struct FeedFile {
+#[derive(Debug, PartialEq, Eq, Clone, Copy)]
+enum FilePosition {
+    Coarse(isize),       // bottom of this item is at bottom of screen
+    Fine(isize, usize),  // line #n of this item is just off bottom of screen
+}
+
+struct FeedFileContents {
     id: FeedId,
+    origin: isize,
     items: Vec<Box<dyn TextFragment>>,
 }
 
+impl FeedFileContents {
+    fn update_items(&mut self, client: &mut Client) {
+        // FIXME: deal with origin, and with the feed having been
+        // extended since we last looked
+
+        let feed = client.borrow_feed(&self.id);
+        let ids: Vec<_> = feed.ids.iter().map(|x| x.clone()).collect();
+        self.origin = feed.origin;
+        std::mem::drop(feed);
+
+        self.items.clear();
+        for id in ids {
+            let st = client.status_by_id(&id)
+                .expect("Any id stored in a Feed should also be cached")
+                .clone();
+            self.items.push(Box::new(StatusDisplay::new(st, client)));
+        }
+    }
+
+    fn index_limit(&self) -> isize {
+        self.origin.checked_add_unsigned(self.items.len())
+            .expect("Out-of-range index")
+    }
+    fn last_index(&self) -> isize { self.index_limit() -1 }
+
+    fn phys_index(&self, index: isize) -> usize {
+        assert!(index >= self.origin, "Index before start");
+        (index - self.origin) as usize
+    }
+
+    fn get(&self, index: isize) -> &Box<dyn TextFragment> {
+        let index = self.phys_index(index);
+        &self.items[index]
+    }
+}
+
+struct FeedFile {
+    contents: FeedFileContents,
+    rendered: HashMap<isize, Vec<ColouredString>>,
+    pos: FilePosition,
+    last_width: Option<usize>,
+}
+
 impl FeedFile {
     fn new(id: FeedId, client: &mut Client) -> Result<Self, ClientError> {
         client.fetch_feed(&id, FeedExtend::Initial)?;
 
-        let mut ff = FeedFile {
+        let mut contents = FeedFileContents {
             id: id,
+            origin: 0,
             items: Vec::new(),
         };
 
-        ff.update_items(client);
+        contents.update_items(client);
 
+        // FIXME: once we have an LDB, that's where initial pos comes from
+        let initial_pos = FilePosition::Coarse(contents.last_index() as isize);
+
+        let ff = FeedFile {
+            contents: contents,
+            rendered: HashMap::new(),
+            pos: initial_pos,
+            last_width: None,
+        };
         Ok(ff)
     }
 
-    fn update_items(&mut self, client: &mut Client) {
-        let ids: Vec<_> = client.borrow_feed(&self.id).ids
-            .iter().map(|x| x.clone()).collect();
-        self.items.clear();
-        for id in ids {
-            let st = client.status_by_id(&id)
-                .expect("Any id stored in a Feed should also be cached")
-                .clone();
-            self.items.push(Box::new(StatusDisplay::new(st, client)));
+    fn ensure_item_rendered(&mut self, index: isize, w: usize) ->
+        &Vec<ColouredString>
+    {
+        if !self.rendered.contains_key(&index) {
+            let mut lines = Vec::new();
+
+            for line in self.contents.get(index).render(w) {
+                for frag in line.split(w) {
+                    lines.push(frag.to_owned());
+                }
+            }
+
+            self.rendered.insert(index, lines);
         }
+
+        self.rendered.get(&index).expect("We just made sure this was present")
+    }
+
+    fn refine_pos(&mut self, w: usize) -> (isize, usize) {
+        let (newpos, item, line) = match self.pos {
+            pos @ FilePosition::Fine(item, line) => (pos, item, line),
+            FilePosition::Coarse(item) => {
+                let rendered = self.ensure_item_rendered(item, w);
+                let line = rendered.len();
+                (FilePosition::Fine(item, line), item, line)
+            }
+        };
+
+        self.pos = newpos;
+        (item, line)
+    }
+
+    fn coarsen_pos(&mut self) {
+        let newpos = match self.pos {
+            pos @ FilePosition::Coarse(_) => pos,
+            FilePosition::Fine(item, _line) => FilePosition::Coarse(item),
+        };
+
+        self.pos = newpos;
     }
 }
 
 impl ActivityState for FeedFile {
+    fn resize(&mut self, w: usize, h: usize) {
+        if self.last_width != Some(w) {
+            self.last_width = Some(w);
+            self.coarsen_pos();
+            self.rendered.clear();
+        }
+        let (item, line) = self.refine_pos(w);
+
+        let mut item = item;
+        let mut lines_rendered = line;
+        while item > self.contents.origin && lines_rendered + 1 < h {
+            item -= 1;
+            lines_rendered += self.ensure_item_rendered(item, w).len();
+        }
+    }
+
     fn draw(&self, w: usize, h: usize)
             -> (Vec<ColouredString>, CursorPosition) {
+        assert_eq!(self.last_width, Some(w), "resize() should have done that");
+        let (start_item, start_line) = match self.pos {
+            FilePosition::Fine(item, line) => (item, line),
+            _ => panic!("coarse position reached draw()"),
+        };
+
+        let mut item = start_item;
         let mut lines = Vec::new();
 
-        'outer: for item in &self.items {
-            for line in item.render(w) {
-                for frag in line.split(w) {
-                    lines.push(frag.to_owned());
-                    if lines.len() + 1 >= h {
-                        break 'outer;
-                    }
+        'outer: while item >= self.contents.origin && lines.len() + 1 < h {
+            let rendered = self.rendered.get(&item)
+                .expect("unrendered item reached draw()");
+            let line_limit = if item == start_item {
+                start_line
+            } else {
+                rendered.len()
+            };
+
+            for i in (0..line_limit).rev() {
+                lines.push(rendered[i].clone());
+                if lines.len() + 1 >= h {
+                    break 'outer;
                 }
             }
+            item -= 1;
+        }
+        lines.reverse();
+
+        while lines.len() + 1 < h {
+            lines.extend_from_slice(&BlankLine::render_static());
         }
 
-        (lines, CursorPosition::None) // FIXME
+        (lines, CursorPosition::None) // FIXME: status line
     }
 
     fn handle_keypress(&mut self, _key: OurKey, _client: &mut Client) ->
index 681305810138472609d33c4e8ecfa669bb23db15..1fab0f74eb26573b6c5210d0bcd1ccac894919c9 100644 (file)
@@ -451,5 +451,9 @@ impl TuiLogicalState {
     fn changed_activity(&mut self, client: &mut Client) {
         self.activity_state = new_activity_state(
             self.activity_stack.top(), client);
+        if let Some(area) = self.last_area {
+            self.activity_state.resize(
+                area.width as usize, area.height as usize);
+        }
     }
 }