From ab099b524a504ff3b5e0a3d3dbac347f9e2551b6 Mon Sep 17 00:00:00 2001 From: Manos Pitsidianakis Date: Wed, 18 Jul 2018 10:42:52 +0300 Subject: [PATCH] Add more documentation. --- .gdbinit | 2 + src/bin.rs | 6 ++ src/ui/cells.rs | 11 ++- src/ui/components/mail.rs | 144 ++++++++++++++++------------- src/ui/components/mod.rs | 11 ++- src/ui/components/notifications.rs | 5 + src/ui/components/utilities.rs | 18 ++++ src/ui/execute/mod.rs | 10 +- src/ui/mod.rs | 138 +++++++++++++-------------- src/ui/position.rs | 69 ++++---------- 10 files changed, 221 insertions(+), 193 deletions(-) create mode 100644 .gdbinit diff --git a/.gdbinit b/.gdbinit new file mode 100644 index 000000000..dbc6aca42 --- /dev/null +++ b/.gdbinit @@ -0,0 +1,2 @@ +break rust_panic +break core::option::expect_failed::h4927e1fef06c4878 diff --git a/src/bin.rs b/src/bin.rs index 96299f8f7..c2e8c578b 100644 --- a/src/bin.rs +++ b/src/bin.rs @@ -18,6 +18,12 @@ * You should have received a copy of the GNU General Public License * along with meli. If not, see . */ + +/*! This crate contains the frontend stuff of the application. The application entry way on `src/bin.rs` creates an event loop and passes input to the `ui` module. + +The mail handling stuff is done in the `melib` crate which includes all backend needs. The split is done to theoretically be able to create different frontends with the same innards. + */ + extern crate melib; #[macro_use] extern crate nom; diff --git a/src/ui/cells.rs b/src/ui/cells.rs index 0e7670c43..04e987c09 100644 --- a/src/ui/cells.rs +++ b/src/ui/cells.rs @@ -1,7 +1,14 @@ +/*! + Define a (x, y) point in the terminal display as a holder of a character, foreground/background + colors and attributes. + */ use std::ops::{Index, IndexMut, Deref, DerefMut}; use super::position::*; use termion::color::AnsiValue; + +/// Types and implementations taken from rustty for convenience. + pub trait CellAccessor: HasSize { fn cellvec(&self) -> &Vec; fn cellvec_mut(&mut self) -> &mut Vec; @@ -409,10 +416,10 @@ impl Color { pub fn as_termion(&self) -> AnsiValue { match *self { b @ Color::Black | b @ Color::Red | b @ Color::Green | b @ Color::Yellow | b @ Color::Blue | b @ Color::Magenta | b @ Color::Cyan | b @ Color::White | b @ Color::Default => - { + { AnsiValue(b.as_byte()) }, - Color::Byte(b) => { + Color::Byte(b) => { AnsiValue(b as u8) }, } diff --git a/src/ui/components/mail.rs b/src/ui/components/mail.rs index 1843c6668..c32772024 100644 --- a/src/ui/components/mail.rs +++ b/src/ui/components/mail.rs @@ -1,3 +1,5 @@ +/*! Entities that handle Mail specific functions. + */ use ui::components::*; use ui::cells::*; @@ -90,10 +92,10 @@ impl MailListing { match iter.peek() { Some(&(_, x)) if mailbox.get_thread(*x).get_indentation() == indentation => - { - indentations.pop(); - indentations.push(true); - } + { + indentations.pop(); + indentations.push(true); + } _ => { indentations.pop(); indentations.push(false); @@ -115,8 +117,8 @@ impl MailListing { Color::Byte(236) } else { Color::Default - } - let x = write_string_to_grid(&MailListing::make_thread_entry(envelope, idx, indentation, container, idx == self.cursor_pos.2, &indentations), + }; + let x = write_string_to_grid(&MailListing::make_thread_entry(envelope, idx, indentation, container, &indentations), &mut content, fg_color, bg_color, @@ -129,23 +131,23 @@ impl MailListing { match iter.peek() { Some(&(_, x)) if mailbox.get_thread(*x).get_indentation() > indentation => - { - indentations.push(false); - } + { + indentations.push(false); + } Some(&(_, x)) if mailbox.get_thread(*x).get_indentation() < indentation => - { - for _ in 0..(indentation - mailbox.get_thread(*x).get_indentation()) { - indentations.pop(); + { + for _ in 0..(indentation - mailbox.get_thread(*x).get_indentation()) { + indentations.pop(); + } } - } _ => {} } } } else { - // Populate `CellBuffer` with every entry. - // TODO: Lazy load? + // Populate `CellBuffer` with every entry. + // TODO: Lazy load? let mut idx = 0; for y in 0..=self.length { if idx >= self.length { @@ -254,6 +256,40 @@ impl MailListing { context.dirty_areas.push_back(area); } + /// Create a pager for the `Envelope` currently under the cursor. + fn draw_header_view(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) { + { + let threaded = context.accounts[self.cursor_pos.0].settings.threaded; + let mailbox = &mut context.accounts[self.cursor_pos.0][self.cursor_pos.1].as_ref().unwrap().as_ref().unwrap(); + let envelope: &Envelope = if threaded { + let i = mailbox.get_threaded_mail(self.cursor_pos.2); + &mailbox.collection[i] + } else { + &mailbox.collection[self.cursor_pos.2] + }; + + let pager_filter = context.settings.pager.filter.clone(); + self.pager = Some(Pager::new(&envelope, pager_filter)); + } + self.pager.as_mut().map(|p| p.draw(grid, area, context)); + } + /// Create a pager for the `Envelope` currently under the cursor. + fn draw_attachment_view(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) { + { + let threaded = context.accounts[self.cursor_pos.0].settings.threaded; + let mailbox = &mut context.accounts[self.cursor_pos.0][self.cursor_pos.1].as_ref().unwrap().as_ref().unwrap(); + let envelope: &Envelope = if threaded { + let i = mailbox.get_threaded_mail(self.cursor_pos.2); + &mailbox.collection[i] + } else { + &mailbox.collection[self.cursor_pos.2] + }; + + let pager_filter = context.settings.pager.filter.clone(); + self.pager = Some(Pager::new(&envelope, pager_filter)); + } + self.pager.as_mut().map(|p| p.draw(grid, area, context)); + } /// Create a pager for the `Envelope` currently under the cursor. fn draw_mail_view(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) { { @@ -272,28 +308,12 @@ impl MailListing { self.pager.as_mut().map(|p| p.draw(grid, area, context)); } fn make_thread_entry(envelope: &Envelope, idx: usize, indent: usize, - container: &Container, highlight: bool, indentations: &Vec) -> String { + container: &Container, indentations: &Vec) -> String { let has_sibling = container.has_sibling(); let has_parent = container.has_parent(); let show_subject = container.get_show_subject(); - let fg_color = if !envelope.is_seen() { - Color::Byte(0) - } else { - Color::Default - }; - let bg_color = if highlight { - if !envelope.is_seen() { - Color::Byte(252) - } else if idx % 2 == 0 { - Color::Byte(236) - } else { - Color::Default - } - } else { - Color::Byte(246) - }; - let mut s = format!("{} {} ", idx, &envelope.get_datetime().format("%Y-%m-%d %H:%M:%S").to_string()); // {} {:.85}",idx,),e.get_subject()) + let mut s = format!("{} {} ", idx, &envelope.get_datetime().format("%Y-%m-%d %H:%M:%S").to_string()); for i in 0..indent { if indentations.len() > i && indentations[i] { @@ -398,50 +418,50 @@ impl Component for MailListing { }; let x = write_string_to_grid(&format!("Date: {}", envelope.get_date_as_str()), - grid, - Color::Byte(33), - Color::Default, - (set_y(upper_left, mid+1), set_y(bottom_right, mid+1))); + grid, + Color::Byte(33), + Color::Default, + (set_y(upper_left, mid+1), set_y(bottom_right, mid+1))); for x in x..=get_x(bottom_right) { grid[(x, mid+1)].set_ch(' '); grid[(x, mid+1)].set_bg(Color::Default); grid[(x, mid+1)].set_fg(Color::Default); } let x = write_string_to_grid(&format!("From: {}", envelope.get_from()), - grid, - Color::Byte(33), - Color::Default, - (set_y(upper_left, mid+2), set_y(bottom_right, mid+2))); + grid, + Color::Byte(33), + Color::Default, + (set_y(upper_left, mid+2), set_y(bottom_right, mid+2))); for x in x..=get_x(bottom_right) { grid[(x, mid+2)].set_ch(' '); grid[(x, mid+2)].set_bg(Color::Default); grid[(x, mid+2)].set_fg(Color::Default); } let x = write_string_to_grid(&format!("To: {}", envelope.get_to()), - grid, - Color::Byte(33), - Color::Default, - (set_y(upper_left, mid+3), set_y(bottom_right, mid+3))); + grid, + Color::Byte(33), + Color::Default, + (set_y(upper_left, mid+3), set_y(bottom_right, mid+3))); for x in x..=get_x(bottom_right) { grid[(x, mid+3)].set_ch(' '); grid[(x, mid+3)].set_bg(Color::Default); grid[(x, mid+3)].set_fg(Color::Default); } let x = write_string_to_grid(&format!("Subject: {}", envelope.get_subject()), - grid, - Color::Byte(33), - Color::Default, - (set_y(upper_left, mid+4), set_y(bottom_right, mid+4))); + grid, + Color::Byte(33), + Color::Default, + (set_y(upper_left, mid+4), set_y(bottom_right, mid+4))); for x in x..=get_x(bottom_right) { grid[(x, mid+4)].set_ch(' '); grid[(x, mid+4)].set_bg(Color::Default); grid[(x, mid+4)].set_fg(Color::Default); } let x = write_string_to_grid(&format!("Message-ID: {}", envelope.get_message_id_raw()), - grid, - Color::Byte(33), - Color::Default, - (set_y(upper_left, mid+5), set_y(bottom_right, mid+5))); + grid, + Color::Byte(33), + Color::Default, + (set_y(upper_left, mid+5), set_y(bottom_right, mid+5))); for x in x..=get_x(bottom_right) { grid[(x, mid+5)].set_ch(' '); grid[(x, mid+5)].set_bg(Color::Default); @@ -588,7 +608,7 @@ impl AccountMenu { let highlight = self.cursor.map(|(x,_)| x == a.index).unwrap_or(false); - + let mut parents: Vec> = vec!(None; a.entries.len()); for (idx, e) in a.entries.iter().enumerate() { @@ -648,9 +668,9 @@ impl AccountMenu { }; let color_fg = if highlight { if idx > 1 && self.cursor.unwrap().1 == idx - 2 { - Color::Byte(233) + Color::Byte(233) } else { - Color::Byte(15) + Color::Byte(15) } } else { Color::Default @@ -658,19 +678,19 @@ impl AccountMenu { let color_bg = if highlight { if idx > 1 && self.cursor.unwrap().1 == idx - 2 { - Color::Byte(15) + Color::Byte(15) } else { - Color::Byte(233) + Color::Byte(233) } } else { Color::Default }; let x = write_string_to_grid(&s, - grid, - color_fg, - color_bg, - (set_y(upper_left, y), bottom_right)); + grid, + color_fg, + color_bg, + (set_y(upper_left, y), bottom_right)); if highlight && idx > 1 && self.cursor.unwrap().1 == idx - 2 { change_colors(grid, ((x, y),(get_x(bottom_right)+1, y)), color_fg , color_bg); diff --git a/src/ui/components/mod.rs b/src/ui/components/mod.rs index 569b31e6f..3f85cc0e0 100644 --- a/src/ui/components/mod.rs +++ b/src/ui/components/mod.rs @@ -19,6 +19,12 @@ * along with meli. If not, see . */ +/*! + Components are ways to handle application data. They can draw on the terminal and receive events, but also do other stuff as well. (For example, see the `notifications` module.) + + See the `Component` Trait for more details. + */ + pub mod utilities; pub mod mail; pub mod notifications; @@ -78,7 +84,7 @@ pub trait Component { } } -/// Copy Area src to dest +/// Copy a source `Area` to a destination. pub fn copy_area(grid_dest: &mut CellBuffer, grid_src: &CellBuffer, dest: Area, src: Area) { if !is_valid_area!(dest) || !is_valid_area!(src) { eprintln!("BUG: Invalid areas in copy_area:\n src: {:?}\n dest: {:?}", src, dest); @@ -104,6 +110,7 @@ pub fn copy_area(grid_dest: &mut CellBuffer, grid_src: &CellBuffer, dest: Area, } } +/// Change foreground and background colors in an `Area` pub fn change_colors(grid: &mut CellBuffer, area: Area, fg_color: Color, bg_color: Color) { if !is_valid_area!(area) { eprintln!("BUG: Invalid area in change_colors:\n area: {:?}", area); @@ -118,6 +125,7 @@ pub fn change_colors(grid: &mut CellBuffer, area: Area, fg_color: Color, bg_colo } +/// Write an `&str` to a `CellBuffer` in a specified `Area` with the passed colors. fn write_string_to_grid(s: &str, grid: &mut CellBuffer, fg_color: Color, bg_color: Color, area: Area) -> usize { let bounds = grid.size(); let upper_left = upper_left!(area); @@ -144,6 +152,7 @@ fn write_string_to_grid(s: &str, grid: &mut CellBuffer, fg_color: Color, bg_colo x } +/// Completely clear an `Area` with an empty char and the terminal's default colors. fn clear_area(grid: &mut CellBuffer, area: Area) { let upper_left = upper_left!(area); let bottom_right = bottom_right!(area); diff --git a/src/ui/components/notifications.rs b/src/ui/components/notifications.rs index 61b01556e..31497f08f 100644 --- a/src/ui/components/notifications.rs +++ b/src/ui/components/notifications.rs @@ -1,7 +1,12 @@ +/*! + Notification handling components. + */ use notify_rust::Notification as notify_Notification; use ui::*; use ui::components::*; + +/// Passes notifications to the OS using the XDG specifications. pub struct XDGNotifications {} impl Component for XDGNotifications { diff --git a/src/ui/components/utilities.rs b/src/ui/components/utilities.rs index 13c8054fa..3324761c6 100644 --- a/src/ui/components/utilities.rs +++ b/src/ui/components/utilities.rs @@ -1,3 +1,6 @@ +/*! Various useful components that can be used in a generic fashion. + */ + use ui::components::*; use ui::cells::*; @@ -125,6 +128,7 @@ pub struct Pager { content: CellBuffer, } +// TODO: Make the `new` method content agnostic. impl Pager { pub fn new(mail: &Envelope, pager_filter: Option) -> Self { let mut text = mail.get_body().get_text(); @@ -159,6 +163,20 @@ impl Pager { content: content, } } + pub fn new_from_str(s: &str) -> Self { + let lines: Vec<&str> = s.trim().split('\n').collect(); + let height = lines.len(); + let width = lines.iter().map(|l| l.len()).max().unwrap_or(0); + let mut content = CellBuffer::new(width, height, Cell::with_char(' ')); + Pager::print_string(&mut content, s); + Pager { + cursor_pos: 0, + height: height, + width: width, + dirty: true, + content: content, + } + } pub fn print_string(content: &mut CellBuffer, s: &str) { let lines: Vec<&str> = s.trim().split('\n').collect(); let width = lines.iter().map(|l| l.len()).max().unwrap_or(0); diff --git a/src/ui/execute/mod.rs b/src/ui/execute/mod.rs index 3aebe5651..be949f4ac 100644 --- a/src/ui/execute/mod.rs +++ b/src/ui/execute/mod.rs @@ -1,11 +1,13 @@ +/*! A parser module for user commands passed through the Ex mode. +*/ use std; use nom::digit; named!(usize_c, -map_res!(map_res!(ws!(digit), std::str::from_utf8), std::str::FromStr::from_str)); + map_res!(map_res!(ws!(digit), std::str::from_utf8), std::str::FromStr::from_str)); named!(pub goto, - preceded!(tag!("b "), - call!(usize_c)) - ); + preceded!(tag!("b "), + call!(usize_c)) + ); diff --git a/src/ui/mod.rs b/src/ui/mod.rs index d6a25b72d..ce2ff32fe 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -19,57 +19,41 @@ * along with meli. If not, see . */ + +/*! + The UI module has an Entity-Component-System design. The System part, is also the application's state, so they're both merged in the `State` struct. + + `State` owns all the Entities of the UI, which are currently plain Containers for `Component`s. In the application's main event loop, input is handed to the state in the form of `UIEvent` objects which traverse the entity graph. Components decide to handle each input or not. + + Input is received in the main loop from threads which listen on the stdin for user input, observe folders for file changes etc. The relevant struct is `ThreadEvent`. + */ + #[macro_use] -pub mod position; +mod position; pub mod components; -pub mod cells; +mod cells; #[macro_use] mod execute; use self::execute::goto; - -extern crate termion; -extern crate melib; - -use std::collections::VecDeque; -use std::fmt; - pub use self::position::*; - -use melib::*; - - -use std; -use termion::{clear, style, cursor}; -use termion::raw::IntoRawMode; -use termion::event::{Key as TermionKey, }; - -use chan::Sender; - - -use std::io::{Write, }; -use termion::input::TermRead; - use self::cells::*; pub use self::components::*; -/* Color pairs; foreground && background. */ -/// Default color. -pub static COLOR_PAIR_DEFAULT: i16 = 1; -/// Highlighted cursor line in index view. -pub static COLOR_PAIR_CURSOR: i16 = 2; -/// Header colour in pager view. -pub static COLOR_PAIR_HEADERS: i16 = 3; -/// Indentation symbol color in index view. -pub static COLOR_PAIR_THREAD_INDENT: i16 = 4; -/// Line color for odd entries in index view. -pub static COLOR_PAIR_THREAD_ODD: i16 = 5; -/// Line color for even entries in index view. -pub static COLOR_PAIR_THREAD_EVEN: i16 = 6; -/// Line color for unread odd entries in index view. -pub static COLOR_PAIR_UNREAD_ODD: i16 = 7; -/// Line color for unread even entries in index view. -pub static COLOR_PAIR_UNREAD_EVEN: i16 = 8; +extern crate melib; +use melib::*; + +use std; +use std::io::{Write, }; +use std::collections::VecDeque; +use std::fmt; +extern crate termion; +use termion::{clear, style, cursor}; +use termion::raw::IntoRawMode; +use termion::event::{Key as TermionKey, }; +use termion::input::TermRead; + +use chan::Sender; /// `ThreadEvent` encapsulates all of the possible values we need to transfer between our threads /// to the main process. @@ -103,6 +87,7 @@ pub enum UIEventType { } +/// An event passed from `State` to its Entities. #[derive(Debug)] pub struct UIEvent { pub id: u64, @@ -124,6 +109,7 @@ impl fmt::Display for UIMode { } } +/// An event notification that is passed to Entities for handling. pub struct Notification { title: String, content: String, @@ -131,6 +117,7 @@ pub struct Notification { timestamp: std::time::Instant, } +/// A context container for loaded settings, accounts, UI changes, etc. pub struct Context { pub accounts: Vec, settings: Settings, @@ -149,6 +136,8 @@ impl Context { } +/// A State object to manage and own components and entities of the UI. `State` is responsible for +/// managing the terminal and interfacing with `melib` pub struct State { cols: usize, rows: usize, @@ -285,24 +274,22 @@ impl State { pub fn register_entity(&mut self, entity: Entity) { self.entities.push(entity); } + /// Convert user commands to actions/method calls. fn parse_command(&mut self, cmd: String) { + //TODO: Make ex mode useful eprintln!("received command: {}", cmd); let result = goto(&cmd.as_bytes()).to_full_result(); eprintln!("result is {:?}", result); if let Ok(v) = result { - self.refresh_mailbox(0, v); - - } - } pub fn rcv_event(&mut self, event: UIEvent) { match event.event_type { - // Command type is only for the State itself. + // Command type is handled only by State. UIEventType::Command(cmd) => { self.parse_command(cmd); return; @@ -331,29 +318,8 @@ impl State { } } -pub fn convert_key(k: TermionKey ) -> Key { - match k { - TermionKey::Backspace => Key::Backspace, - TermionKey::Left => Key::Left, - TermionKey::Right => Key::Right, - TermionKey::Up => Key::Up, - TermionKey::Down => Key::Down, - TermionKey::Home => Key::Home, - TermionKey::End => Key::End, - TermionKey::PageUp => Key::PageUp, - TermionKey::PageDown => Key::PageDown, - TermionKey::Delete => Key::Delete, - TermionKey::Insert => Key::Insert, - TermionKey::F(u) => Key::F(u), - TermionKey::Char(c) => Key::Char(c), - TermionKey::Alt(c) => Key::Alt(c), - TermionKey::Ctrl(c) => Key::Ctrl(c), - TermionKey::Null => Key::Null, - TermionKey::Esc => Key::Esc, - _ => Key::Char(' '), - } -} +// TODO: Pass Ctrl C etc to the terminal. #[derive(Debug)] pub enum Key { /// Backspace. @@ -396,12 +362,36 @@ pub enum Key { Esc, } -pub fn get_events(stdin: std::io::Stdin, mut closure: F) where F: FnMut(Key) -> (){ - let stdin = stdin.lock(); - for c in stdin.keys() { - if let Ok(k) = c { - let k = convert_key(k); - closure(k); +impl From for Key { + fn from(k: TermionKey ) -> Self { + match k { + TermionKey::Backspace => Key::Backspace, + TermionKey::Left => Key::Left, + TermionKey::Right => Key::Right, + TermionKey::Up => Key::Up, + TermionKey::Down => Key::Down, + TermionKey::Home => Key::Home, + TermionKey::End => Key::End, + TermionKey::PageUp => Key::PageUp, + TermionKey::PageDown => Key::PageDown, + TermionKey::Delete => Key::Delete, + TermionKey::Insert => Key::Insert, + TermionKey::F(u) => Key::F(u), + TermionKey::Char(c) => Key::Char(c), + TermionKey::Alt(c) => Key::Alt(c), + TermionKey::Ctrl(c) => Key::Ctrl(c), + TermionKey::Null => Key::Null, + TermionKey::Esc => Key::Esc, + _ => Key::Char(' '), + } + } +} + +pub fn get_events(stdin: std::io::Stdin, mut closure: impl FnMut(Key)) -> (){ + let stdin = stdin.lock(); + for c in stdin.keys() { + if let Ok(k) = c { + closure(Key::from(k)); } } } diff --git a/src/ui/position.rs b/src/ui/position.rs index 0fe342e81..844e559fc 100644 --- a/src/ui/position.rs +++ b/src/ui/position.rs @@ -1,3 +1,9 @@ +/*! + Simple type definitions and macro helper for a (x,y) position on the terminal and the areas they define. + + An `Area` consists of two points: the upper left and bottom right corners. + */ + /// A `(x, y)` position on screen. pub type Pos = (usize, usize); @@ -18,6 +24,7 @@ pub fn set_y(p: Pos, new_y: usize) -> Pos { (p.0, new_y) } +/// An `Area` consists of two points: the upper left and bottom right corners. pub type Area = (Pos, Pos); #[macro_export] @@ -25,15 +32,19 @@ macro_rules! upper_left { ($a:expr) => ( $a.0 ) } #[macro_export] macro_rules! bottom_right { ($a:expr) => ( $a.1 ) } #[macro_export] -macro_rules! is_valid_area { ($a:expr) => { { - let upper_left = upper_left!($a); - let bottom_right = bottom_right!($a); - if get_y(upper_left) > get_y(bottom_right) || get_x(upper_left) > get_x(bottom_right) { - false - } else { - true +macro_rules! is_valid_area { ($a:expr) => + { + { + let upper_left = upper_left!($a); + let bottom_right = bottom_right!($a); + if get_y(upper_left) > get_y(bottom_right) || get_x(upper_left) > get_x(bottom_right) { + false + } else { + true + } } - } } } + } +} /// A `(cols, rows)` size. pub type Size = (usize, usize); @@ -46,45 +57,3 @@ pub trait HasPosition { fn origin(&self) -> Pos; fn set_origin(&mut self, new_origin: Pos); } - -/// A cursor position. -pub struct Cursor { - pos: Option, - last_pos: Option, -} - -impl Cursor { - pub fn new() -> Cursor { - Cursor { - pos: None, - last_pos: None, - } - } - - /// Checks whether the current and last coordinates are sequential and returns `true` if they - /// are and `false` otherwise. - pub fn is_seq(&self) -> bool { - if let Some((cx, cy)) = self.pos { - if let Some((lx, ly)) = self.last_pos { - (lx + 1, ly) == (cx, cy) - } else { - false - } - } else { - false - } - } - - pub fn pos(&self) -> Option { - self.pos - } - - pub fn set_pos(&mut self, newpos: Option) { - self.last_pos = self.pos; - self.pos = newpos; - } - - pub fn invalidate_last_pos(&mut self) { - self.last_pos = None; - } -}