diff options
author | Sam Tay <sam.chong.tay@gmail.com> | 2020-06-16 03:02:32 -0700 |
---|---|---|
committer | Sam Tay <sam.chong.tay@gmail.com> | 2020-06-16 03:08:45 -0700 |
commit | 88a3f45defb509daf254efced585e945eb8f05c2 (patch) | |
tree | 773b8d1cea15452c9544322f3e36e83e1530ba30 | |
parent | 2e3964d35141beb37e08a144c9440a687dfec8fb (diff) |
Allow cycling through layouts
-rw-r--r-- | TODO.md | 6 | ||||
-rw-r--r-- | roadmap.md | 2 | ||||
-rw-r--r-- | src/tui/app.rs | 10 | ||||
-rw-r--r-- | src/tui/views.rs | 351 |
4 files changed, 261 insertions, 108 deletions
@@ -10,10 +10,10 @@ changing [soon](https://meta.stackexchange.com/q/348746). #### Cursive interface for viewing questions and answers 2. Handle focus with tab and h,l -4. Allow cycling layouts -5. Init with smaller layout if terminal size smaller +4. Allow cycling layouts? +5. Init with smaller layout if terminal size smaller? maybe cli --auto-resize + option 6. Small text at bottom with '?' to bring up key mapping dialog -7. `/` to bring up new search prompt #### other 1. Use [par_iter](https://github.com/rayon-rs/rayon) for text preprocess & parsing? @@ -24,6 +24,8 @@ [ ] Add duckduckgo scraper ### at some point +[ ] allow new queries from TUI, e.g. hit `/` for a prompt +[ ] or `/` searches current q/a [ ] clean up error.rs and term.rs ; only keep whats actually ergonomic [ ] ask legal@stackoverflow.com for permission to logo stackoverflow/stackexchange in readme [ ] add duckduckgo logo to readme diff --git a/src/tui/app.rs b/src/tui/app.rs index 97249b0..3d00ddc 100644 --- a/src/tui/app.rs +++ b/src/tui/app.rs @@ -1,4 +1,3 @@ -use cursive::event::{Callback, Event, EventResult}; use cursive::theme::{BaseColor, Color, Effect, Style}; use cursive::utils::markup::StyledString; use cursive::utils::span::SpannedString; @@ -10,8 +9,8 @@ use std::sync::Arc; use super::markdown; use super::views::{ - FullLayout, ListView, MdView, Name, NAME_ANSWER_LIST, NAME_ANSWER_VIEW, NAME_FULL_LAYOUT, - NAME_QUESTION_LIST, NAME_QUESTION_VIEW, + LayoutView, ListView, MdView, Name, NAME_ANSWER_LIST, NAME_ANSWER_VIEW, NAME_QUESTION_LIST, + NAME_QUESTION_VIEW, }; use crate::config; use crate::error::Result; @@ -84,7 +83,7 @@ pub fn run(qs: Vec<Question>) -> Result<()> { s.call_on_name(NAME_ANSWER_VIEW, |v: &mut MdView| v.set_content(&a.body)); }); - siv.add_layer(FullLayout::new( + siv.add_layer(LayoutView::new( 1, question_list_view, question_view, @@ -98,7 +97,6 @@ pub fn run(qs: Vec<Question>) -> Result<()> { } cursive::logger::init(); siv.add_global_callback('?', cursive::Cursive::toggle_debug_console); - println!("{:?}", siv.debug_name(NAME_QUESTION_VIEW)); siv.run(); Ok(()) } @@ -132,7 +130,7 @@ fn preview_question(q: &Question) -> StyledString { } fn preview_answer(screen_width: usize, a: &Answer) -> StyledString { - let width = cmp::min(a.body.len(), screen_width / 2); + let width = cmp::min(a.body.len(), screen_width); let md = markdown::preview(width, a.body.to_owned()); let mut preview = pretty_score(a.score); if a.is_accepted { diff --git a/src/tui/views.rs b/src/tui/views.rs index 0d52623..403990d 100644 --- a/src/tui/views.rs +++ b/src/tui/views.rs @@ -3,11 +3,10 @@ use cursive::traits::{Finder, Nameable, Resizable, Scrollable}; use cursive::utils::markup::StyledString; use cursive::view::{Margins, SizeConstraint, View, ViewWrapper}; use cursive::views::{ - LinearLayout, NamedView, OnEventView, PaddedView, Panel, ResizedView, ScrollView, SelectView, - TextView, + HideableView, LinearLayout, NamedView, OnEventView, PaddedView, Panel, ResizedView, ScrollView, + SelectView, TextView, }; -use cursive::{Cursive, Vec2}; -use std::cell::RefCell; +use cursive::{Cursive, Vec2, XY}; use std::fmt; use std::fmt::Display; use std::rc::Rc; @@ -52,17 +51,43 @@ impl From<Name> for String { } } +trait Resize { + fn set_width(&mut self, width: &SizeConstraint); + fn set_height(&mut self, height: &SizeConstraint); + fn resize(&mut self, width: &SizeConstraint, height: &SizeConstraint) { + self.set_width(width); + self.set_height(height); + } +} + +trait Hide { + fn set_visible(&mut self, visible: bool); + fn hide(&mut self) { + self.set_visible(false); + } + fn unhide(&mut self) { + self.set_visible(true); + } +} + // TODO maybe I should use cursive's ListView over SelectView ? -pub type ListView = - ListViewT<ResizedView<Panel<ScrollView<OnEventView<NamedView<SelectView<u32>>>>>>>; +// TODO copy one of them to allow overriding selected style => reverse video +pub type ListView = ListViewT< + HideableView<ResizedView<Panel<ScrollView<OnEventView<NamedView<SelectView<u32>>>>>>>, +>; pub struct ListViewT<T: View> { inner_name: String, view: T, + force_take_focus: bool, } impl<T: View> ViewWrapper for ListViewT<T> { cursive::wrap_impl!(self.view: T); + + fn wrap_take_focus(&mut self, source: cursive::direction::Direction) -> bool { + self.force_take_focus || self.view.take_focus(source) + } } impl ListView { @@ -98,7 +123,13 @@ impl ListView { let view = view.scrollable(); let view = Panel::new(view).title(format!("{}", name)); let view = view.resized(SizeConstraint::Free, SizeConstraint::Free); - let view = ListViewT { view, inner_name }; + let view = HideableView::new(view); + let view = ListViewT { + view, + inner_name, + force_take_focus: false, + }; + view.with_name(name) } @@ -125,20 +156,42 @@ impl ListView { self.view.call_on_name(&self.inner_name, cb).expect("TODO") } - pub fn resize(&mut self, width: SizeConstraint, height: SizeConstraint) { - self.view.set_constraints(width, height); + pub fn set_take_focus(&mut self, take: bool) { + self.force_take_focus = take; } } -pub type MdView = MdViewT<ResizedView<Panel<ScrollView<NamedView<TextView>>>>>; +impl Resize for ListView { + fn set_width(&mut self, width: &SizeConstraint) { + self.view.get_inner_mut().set_width(*width); + } + fn set_height(&mut self, height: &SizeConstraint) { + self.view.get_inner_mut().set_height(*height); + } +} + +impl Hide for ListView { + fn set_visible(&mut self, visible: bool) { + self.view.set_visible(visible); + } +} + +pub type MdView = MdViewT<HideableView<ResizedView<Panel<ScrollView<NamedView<TextView>>>>>>; pub struct MdViewT<T: View> { inner_name: String, view: T, + /// If the LayoutView is in full screen mode, MdView should always accept + /// focus. + force_take_focus: bool, } impl<T: View> ViewWrapper for MdViewT<T> { cursive::wrap_impl!(self.view: T); + + fn wrap_take_focus(&mut self, source: cursive::direction::Direction) -> bool { + self.force_take_focus || self.view.take_focus(source) + } } impl MdView { @@ -148,7 +201,12 @@ impl MdView { let view = view.scrollable(); let view = Panel::new(view); let view = view.resized(SizeConstraint::Free, SizeConstraint::Free); - let view = MdViewT { view, inner_name }; + let view = HideableView::new(view); + let view = MdViewT { + view, + inner_name, + force_take_focus: false, + }; view.with_name(name) } @@ -164,36 +222,64 @@ impl MdView { .expect("unwrap failed in MdView.set_content") } - pub fn resize(&mut self, width: SizeConstraint, height: SizeConstraint) { - self.view.set_constraints(width, height); + pub fn set_take_focus(&mut self, take: bool) { + self.force_take_focus = take; } } -pub type FullLayout = FullLayoutT<PaddedView<LinearLayout>>; +impl Resize for MdView { + fn set_width(&mut self, width: &SizeConstraint) { + self.view.get_inner_mut().set_width(*width); + } + fn set_height(&mut self, height: &SizeConstraint) { + self.view.get_inner_mut().set_height(*height); + } +} -pub struct FullLayoutT<T: View> { - view: T, - invalidated: bool, +impl Hide for MdView { + fn set_visible(&mut self, visible: bool) { + self.view.set_visible(visible); + } } -struct FullLayoutSizing { +pub struct LayoutView { + view: PaddedView<LinearLayout>, + layout: Layout, + layout_invalidated: bool, + size_invalidated: bool, +} + +struct LayoutViewSizing { width: SizeConstraint, list_height: SizeConstraint, view_height: SizeConstraint, } -// TODO set child widths based on parent -impl ViewWrapper for FullLayoutT<PaddedView<LinearLayout>> { +pub enum Layout { + BothColumns, + SingleColumn, + FullScreen, +} + +impl ViewWrapper for LayoutView { cursive::wrap_impl!(self.view: PaddedView<LinearLayout>); // TODO what the actual fuck is wrong with this lifetime? // cursive does this shit all over the place... // For now just issue a call_on_name like an asshat fn wrap_on_event(&mut self, event: Event) -> EventResult { - if let Event::WindowResize = event { - println!("resize event thrown"); - self.invalidated = true; + match event { + Event::WindowResize => { + self.size_invalidated = true; + } + Event::Char(' ') => { + self.cycle_layout(); + self.layout_invalidated = true; + return EventResult::Consumed(None); + } + _ => (), } + self.view.on_event(event) } @@ -203,16 +289,18 @@ impl ViewWrapper for FullLayoutT<PaddedView<LinearLayout>> { fn wrap_layout(&mut self, size: Vec2) { self.resize(size); - self.invalidated = false; + self.relayout(); + self.layout_invalidated = false; + self.size_invalidated = false; self.view.layout(size); } fn wrap_needs_relayout(&self) -> bool { - self.invalidated || self.view.needs_relayout() + self.layout_invalidated || self.size_invalidated || self.view.needs_relayout() } } -impl FullLayout { +impl LayoutView { pub fn new( lr_margin: usize, q_list: NamedView<ListView>, @@ -221,80 +309,159 @@ impl FullLayout { a_view: NamedView<MdView>, ) -> NamedView<Self> { let view = LinearLayout::horizontal() - .child( - // TODO decide whats better, horizontal sizing on the outside, - // or keeping both sizings on the 4 internal views - LinearLayout::vertical().child(q_list).child(q_view), - ) + .child(LinearLayout::vertical().child(q_list).child(q_view)) .child(LinearLayout::vertical().child(a_list).child(a_view)); let view = PaddedView::new(Margins::lrtb(lr_margin, lr_margin, 0, 0), view); - (FullLayoutT { + (LayoutView { view, - invalidated: true, + layout_invalidated: true, + size_invalidated: true, + layout: Layout::BothColumns, // TODO choose this based on initial width? }) .with_name(NAME_FULL_LAYOUT) } - // public for now TODO remove - pub fn resize(&mut self, size: Vec2) { - let FullLayoutSizing { + fn get_constraints(&self, screen_size: Vec2) -> LayoutViewSizing { + let heuristic = 1; + let width = SizeConstraint::Fixed(screen_size.x / 2 - heuristic); + let list_height = SizeConstraint::AtMost(screen_size.y / 3); + let view_height = SizeConstraint::Full; + + LayoutViewSizing { + width, + list_height, + view_height, + } + } + + // TODO wtf is going on here + fn resize(&mut self, size: Vec2) { + let LayoutViewSizing { width, list_height, view_height, } = self.get_constraints(size); + self.call_on_list_views(move |v| v.resize(&width, &list_height)); + self.call_on_md_views(move |v| v.resize(&width, &view_height)); + } + + fn relayout(&mut self) { + match self.layout { + Layout::BothColumns => { + self.call_on_list_views(|v| v.set_take_focus(false)); + self.call_on_md_views(|v| v.set_take_focus(false)); + self.call_on_list_views(|v| v.unhide()); + self.call_on_md_views(|v| v.unhide()); + } + // TODO see if call on column works + Layout::SingleColumn => { + self.call_on_list_views(|v| v.set_take_focus(true)); + self.call_on_md_views(|v| { + v.hide(); + v.set_width(&SizeConstraint::Full); + }); + self.call_on_list_views(|v| { + v.hide(); + v.set_width(&SizeConstraint::Full); + }); + match self.get_focused_index().x { + 0 => { + self.view + .call_on_name(NAME_QUESTION_LIST, |v: &mut ListView| { + v.unhide(); + }); + self.view + .call_on_name(NAME_QUESTION_VIEW, |v: &mut MdView| { + v.unhide(); + }); + } + _ => { + self.view + .call_on_name(NAME_ANSWER_LIST, |v: &mut ListView| { + v.unhide(); + }); + self.view.call_on_name(NAME_ANSWER_VIEW, |v: &mut MdView| { + v.unhide(); + }); + } + }; + } + Layout::FullScreen => { + self.call_on_md_views(|v| v.set_take_focus(true)); + self.call_on_md_views(|v| { + v.hide(); + v.resize(&SizeConstraint::Full, &SizeConstraint::Full); + }); + self.call_on_list_views(|v| { + v.hide(); + v.resize(&SizeConstraint::Full, &SizeConstraint::Full); + }); + let name = Self::xy_to_name(self.get_focused_index()); + if name == NAME_QUESTION_LIST || name == NAME_ANSWER_LIST { + self.view.call_on_name(name, |v: &mut ListView| { + v.unhide(); + }); + } else { + self.view.call_on_name(name, |v: &mut MdView| { + v.unhide(); + }); + } + } + } + } + + fn cycle_layout(&mut self) { + self.layout = match self.layout { + Layout::BothColumns => Layout::SingleColumn, + Layout::SingleColumn => Layout::FullScreen, + Layout::FullScreen => Layout::BothColumns, + } + } + + fn call_on_list_views<F>(&mut self, f: F) -> () + where + F: Fn(&mut ListView) + 'static, + { + let f: Rc<dyn Fn(&mut ListView)> = Rc::new(move |v| f(v)); self.view - .call_on_name(NAME_QUESTION_LIST, |v: &mut ListView| { - v.resize(width, list_height) - }) - .expect("TODO"); + .call_on_name(NAME_QUESTION_LIST, &*f) + .expect("TODO: call on question list failed"); self.view - .call_on_name(NAME_ANSWER_LIST, |v: &mut ListView| { - v.resize(width, list_height) - }) - .expect("TODO"); + .call_on_name(NAME_ANSWER_LIST, &*f) + .expect("TODO: call on answer list failed"); + } + + fn call_on_md_views<F>(&mut self, f: F) -> () + where + F: Fn(&mut MdView) + 'static, + { + let f: Rc<dyn Fn(&mut MdView)> = Rc::new(move |v| f(v)); self.view - .call_on_name(NAME_QUESTION_VIEW, |v: &mut MdView| { - v.resize(width, view_height) - }) - .expect("TODO"); + .call_on_name(NAME_QUESTION_VIEW, &*f) + .expect("TODO: call on question view failed"); self.view - .call_on_name(NAME_ANSWER_VIEW, |v: &mut MdView| { - v.resize(width, view_height) - }) - .expect("TODO"); - //.and_then(View::downcast_mut) - //.map(View::downcast_mut) - //.map(|v: &mut LinearLayout| { - //println!("downcast successful!"); - //v.get_child_mut(0).and_then(View::downcast_mut).map( - //|v: &mut ResizedView<NamedView<ListView>>| { - //println!("LIST CONSTRAINTS SET"); - //v.set_constraints(width, list_height); - //}, - //); - //v.get_child_mut(1).and_then(View::downcast_mut).map( - //|v: &mut ResizedView<NamedView<ListView>>| { - //println!("VIEW CONSTRAINTS SET"); - //v.set_constraints(width, list_height); - //}, - //) - //}); - } - - fn get_constraints(&self, screen_size: Vec2) -> FullLayoutSizing { - let heuristic = 1; - let width = SizeConstraint::Fixed(screen_size.x / 2 - heuristic); - let list_height = SizeConstraint::AtMost(screen_size.y / 3); - let view_height = SizeConstraint::Full; //AtLeast(2 * screen_size.y / 3); - println!( - "list constraints: {} x {}", - screen_size.x / 2 - heuristic, - screen_size.y / 3 - ); - FullLayoutSizing { - width, - list_height, - view_height, + .call_on_name(NAME_ANSWER_VIEW, &*f) + .expect("TODO: call on answer view failed"); + } + + fn get_focused_index(&self) -> Vec2 { + let top = self.view.get_inner(); + let x = top.get_focus_index(); + let inner = top + .get_child(x) + .unwrap() + .downcast_ref::<LinearLayout>() + .unwrap(); + let y = inner.get_focus_index(); + XY { x, y } + } + + fn xy_to_name(xy: Vec2) -> &'static str { + match xy { + XY { x: 0, y: 0 } => NAME_QUESTION_LIST, + XY { x: 0, y: 1 } => NAME_QUESTION_VIEW, + XY { x: 1, y: 0 } => NAME_ANSWER_LIST, + _ => NAME_ANSWER_VIEW, } } } @@ -312,17 +479,3 @@ where Some(EventResult::Consumed(Some(s.get_mut().select_down(1)))) }) } - -pub enum Layout { - BothColumns, - SingleColumn, - FullScreen, -} - -pub enum Mode { - /// Akin to vim, keys are treated as commands - Normal, - /// Akin to vim, user is typing in bottom prompt - Insert, - // TODO if adding a search feature, that will be anther mode -} |