From 323805b52f26b18232c3c83c6f0c4189c94d6e6b Mon Sep 17 00:00:00 2001 From: Alexandre Bury Date: Sun, 31 May 2015 16:38:53 -0700 Subject: [PATCH] Add scrolling to SelectView Scrolling functionalities are stored in ScrollBase. Both TextView and SelectView use it. Also add letter search to SelectView (currently case sensitive). And made the cities example bigger with a long list of capitals. --- assets/cities.txt | 254 ++++++++++++++++++++++++++++++++++++++++ examples/select.rs | 22 ++-- src/view/mod.rs | 2 + src/view/scroll.rs | 95 +++++++++++++++ src/view/select_view.rs | 69 +++++++++-- src/view/text_view.rs | 54 +++------ 6 files changed, 443 insertions(+), 53 deletions(-) create mode 100644 assets/cities.txt create mode 100644 src/view/scroll.rs diff --git a/assets/cities.txt b/assets/cities.txt new file mode 100644 index 0000000..93f18a4 --- /dev/null +++ b/assets/cities.txt @@ -0,0 +1,254 @@ +Abidjan +Abu Dhabi +Abuja +Accra +Adamstown +Addis Ababa +Algiers +Alofi +Amman +Amsterdam +Andorra la Vella +Ankara +Antananarivo +Apia +Arbil +Ashgabat +Asmara +Astana +Asunción +Athens +Avarua +Baghdad +Baku +Bamako +Bandar Seri Begawan +Bangkok +Bangui +Banjul +Basseterre +Beijing +Beirut +Belgrade +Belmopan +Berlin +Bern +Bishkek +Bissau +Bogotá +Brasília +Bratislava +Brazzaville +Bridgetown +Brussels +Bucharest +Budapest +Buenos Aires +Bujumbura +Cairo +Canberra +Caracas +Cardiff +Castries +Cayenne +Charlotte Amalie +Chișinău +Cockburn Town +Colombo +Conakry +Copenhagen +Dakar +Damascus +Dhaka +Dili +Djibouti +Dodoma +Doha +Douglas +Dublin +Dushanbe +Edinburgh +Fort-de-France +Freetown +Funafuti +Funchal +Gaborone +Garoowe +Gaza +Georgetown +Georgetown +George Town +Gibraltar +Grozny +Guatemala City +Hagatna +Hamilton +Hanoi +Harare +Hargeisa +Havana +Helsinki +Honiara +Islamabad +Jakarta +Jamestown +Jerusalem +Jerusalem +Kabul +Kampala +Kathmandu +Khartoum +Kiev +Kigali +Kilinochchi +Kingston +Kingston +Kingstown +Kinshasa +Kuala Lumpur +Kuwait City +La Paz +Las Palmas +Libreville +Lilongwe +Lima +Lisbon +Ljubljana +Lomé +London +Luanda +Lusaka +Luxembourg +Madrid +Majuro +Malabo +Malé +Mamoudzou +Managua +Manama +Manila +Maputo +Maseru +Mata-Utu +Mbabane +Melekeok +Mexico City +Minsk +Mogadishu +Monaco +Monrovia +Montevideo +Moroni +Moscow +Muscat +Nairobi +Nassau +Naypyidaw +N'Djamena +New Delhi +Niamey +Nicosia +none +Nouakchott +Nouméa +Nuku'alofa +Nuuk +Oranjestad +Oslo +Ottawa +Ouagadougou +Pago Pago +Palikir +Panama City +Papeete +Paramaribo +Paris +Phnom Penh +Plymouth +Podgorica +Ponta Delgada +Port-au-Prince +Port Louis +Port Moresby +Port of Spain +Porto-Novo +Port Vila +Prague +Praia +Pretoria +Priština +Putrajaya +Pyongyang +Quito +Rabat +Ramallah +Reykjavík +Riga +Riyadh +Road Town +Rome +Roseau +Saint-Denis +Saint Helier +Saint-Pierre +Saipan +Sanaa +San Juan +San Marino +San Salvador +Santiago +Sant José +Santo Domingo +São Tomé +Sarajevo +Seoul +Singapore +Skopje +Sofia +Stanley +Stepanakert +St. George's +St. John's +Stockholm +St Peter Port +Sucre +Sukhumi +Suva +Taipei +Tallinn +Tarawa +Tashkent +Tbilisi +Tegucigalpa +Tehran +The Settlement +The Valley +Thimphu +Tirana +Tiraspol +Tokyo +Tórshavn +Tripoli +Tskhinvali +Tunis +Ulaanbaatar +Vaduz +Valletta +Valparaíso­so +Vatican City +Victoria +Vienna +Vientiane +Vilnius +Vitoria-Gasteiz +Warsaw +Washington, D.C. +Wellington +West Island +Willemstad +Windhoek +Yamoussoukro +Yaoundé +Yaren +Yerevan +Zagreb diff --git a/examples/select.rs b/examples/select.rs index 6b7bd71..05b1f27 100644 --- a/examples/select.rs +++ b/examples/select.rs @@ -1,17 +1,25 @@ extern crate cursive; +use std::fs::File; +use std::io::{BufReader,BufRead}; + use cursive::Cursive; -use cursive::view::{Dialog,SelectView,TextView,Selector}; +use cursive::view::{Dialog,SelectView,TextView,Selector,BoxView}; fn main() { + + let mut select = SelectView::new(); + + // Read the list of cities from separate file, and fill the view with it. + let file = File::open("assets/cities.txt").unwrap(); + let reader = BufReader::new(file); + for line in reader.lines() { + select.add_item_str(&line.unwrap()); + } + let mut siv = Cursive::new(); - siv.add_layer(Dialog::new(SelectView::new() - .item_str("Berlin") - .item_str("London") - .item_str("New York") - .item_str("Paris") - .with_id("city")) + siv.add_layer(Dialog::new(BoxView::new((20,10), select.with_id("city"))) .title("Where are you from?") .button("Ok", |s| { let city = s.find::(&Selector::Id("city")).unwrap().selection().to_string(); diff --git a/src/view/mod.rs b/src/view/mod.rs index 04e942b..cbc4995 100644 --- a/src/view/mod.rs +++ b/src/view/mod.rs @@ -15,6 +15,8 @@ mod shadow_view; mod edit_view; mod select_view; +mod scroll; + use std::any::Any; pub use self::view_path::ViewPath; diff --git a/src/view/scroll.rs b/src/view/scroll.rs new file mode 100644 index 0000000..d8fe434 --- /dev/null +++ b/src/view/scroll.rs @@ -0,0 +1,95 @@ +use std::cmp::{min,max}; + +use color; +use vec::Vec2; +use printer::Printer; + +pub struct ScrollBase { + pub start_line: usize, + pub content_height: usize, + pub view_height: usize, +} + +impl ScrollBase { + pub fn new() -> Self { + ScrollBase { + start_line: 0, + content_height: 0, + view_height: 0, + } + } + + pub fn set_heights(&mut self, view_height: usize, content_height: usize) { + self.view_height = view_height; + self.content_height = content_height; + + if self.scrollable() { + self.start_line = min(self.start_line, self.content_height - self.view_height); + } else { + self.start_line = 0; + } + } + + pub fn scrollable(&self) -> bool { + self.view_height < self.content_height + } + + pub fn can_scroll_up(&self) -> bool { + self.start_line > 0 + } + + pub fn can_scroll_down(&self) -> bool { + self.start_line + self.view_height < self.content_height + } + + pub fn scroll_top(&mut self) { + self.start_line = 0; + } + + pub fn scroll_to(&mut self, y: usize) { + if y >= self.start_line + self.view_height { + self.start_line = 1 + y - self.view_height; + } else if y < self.start_line { + self.start_line = y; + } + } + + pub fn scroll_bottom(&mut self) { + self.start_line = self.content_height - self.view_height; + } + + pub fn scroll_down(&mut self, n: usize) { + self.start_line = min(self.start_line+n, self.content_height - self.view_height); + } + + pub fn scroll_up(&mut self, n: usize) { + self.start_line -= min(self.start_line, n); + } + + pub fn draw(&self, printer: &Printer, line_drawer: F) + where F: Fn(&Printer,usize) + { + for y in 0..self.view_height { + line_drawer(&printer.sub_printer(Vec2::new(0,y),printer.size,true), y+self.start_line); + } + + + if self.view_height < self.content_height { + // We directly compute the size of the scrollbar (this allow use to avoid using floats). + // (ratio) * max_height + // Where ratio is ({start or end} / content.height) + let height = max(1,self.view_height * self.view_height / self.content_height); + // Number of different possible positions + let steps = self.view_height - height + 1; + + // Now + let start = steps * self.start_line / (1 + self.content_height - self.view_height); + + printer.with_color( + if printer.focused { color::HIGHLIGHT } else { color::HIGHLIGHT_INACTIVE }, + |printer| { + printer.print_vline((printer.size.x-1, start), height, ' ' as u64); + }); + } + } +} diff --git a/src/view/select_view.rs b/src/view/select_view.rs index e9f8086..0147b80 100644 --- a/src/view/select_view.rs +++ b/src/view/select_view.rs @@ -1,8 +1,11 @@ +use std::cmp::min; + use color; -use view::{View,IdView,SizeRequest}; +use view::{View,IdView,SizeRequest,DimensionRequest}; use event::{Event,EventResult,Key}; use vec::Vec2; use printer::Printer; +use super::scroll::ScrollBase; struct Item { label: String, @@ -24,6 +27,7 @@ impl Item { pub struct SelectView { items: Vec>, focus: usize, + scrollbase: ScrollBase, } impl SelectView { @@ -32,6 +36,7 @@ impl SelectView { SelectView { items: Vec::new(), focus: 0, + scrollbase: ScrollBase::new(), } } @@ -41,8 +46,13 @@ impl SelectView { } /// Adds a item to the list, with given label and value. - pub fn item(mut self, label: &str, value: T) -> Self { + pub fn add_item(&mut self, label: &str, value: T) { self.items.push(Item::new(label,value)); + } + + /// Chainable variant of add_item + pub fn item(mut self, label: &str, value: T) -> Self { + self.add_item(label, value); self } @@ -58,36 +68,73 @@ impl SelectView { pub fn item_str(self, label: &str) -> Self { self.item(label, label.to_string()) } + + pub fn add_item_str(&mut self, label: &str) { + self.add_item(label, label.to_string()); + } } impl View for SelectView { fn draw(&mut self, printer: &Printer) { - for (i,item) in self.items.iter().enumerate() { - let style = if i == self.focus { if printer.focused { color::HIGHLIGHT } else { color::HIGHLIGHT_INACTIVE } } else { color::PRIMARY }; - printer.with_color(style, |printer| printer.print((0,i), &item.label)); - } + + self.scrollbase.draw(printer, |printer,i| { + let style = if i == self.focus { + if printer.focused { color::HIGHLIGHT } + else { color::HIGHLIGHT_INACTIVE } + } else { + color::PRIMARY + }; + printer.with_color(style, |printer| printer.print((0,0), &self.items[i].label)); + }); } - fn get_min_size(&self, _: SizeRequest) -> Vec2 { + fn get_min_size(&self, req: SizeRequest) -> Vec2 { let w = self.items.iter().map(|item| item.label.len()).max().unwrap_or(1); let h = self.items.len(); + + let scrolling = if let DimensionRequest::Fixed(r_h) = req.h { + r_h < h + } else if let DimensionRequest::AtMost(r_h) = req.h { + r_h < h + } else { + false + }; + + let w = if scrolling { w + 1 } else { w }; + Vec2::new(w,h) } fn on_event(&mut self, event: Event) -> EventResult { match event { - Event::KeyEvent(key) => match key { - Key::Up if self.focus > 0 => self.focus -= 1, - Key::Down if self.focus + 1 < self.items.len() => self.focus += 1, - _ => return EventResult::Ignored, + Event::KeyEvent(Key::Up) if self.focus > 0 => self.focus -= 1, + Event::KeyEvent(Key::Down) if self.focus + 1 < self.items.len() => self.focus += 1, + Event::KeyEvent(Key::PageUp) => self.focus -= min(self.focus,10), + Event::KeyEvent(Key::PageDown) => self.focus = min(self.focus+10,self.items.len()-1), + Event::KeyEvent(Key::Home) => self.focus = 0, + Event::KeyEvent(Key::End) => self.focus = self.items.len()-1, + Event::CharEvent(c) => { + // Starting from the current focus, find the first item that match the char. + if let Some((i,_)) = self.items.iter().enumerate() + .skip(self.focus+1) + .find(|&(_,item)| item.label.starts_with(c)) + { + self.focus = i; + } }, _ => return EventResult::Ignored, } + self.scrollbase.scroll_to(self.focus); + EventResult::Consumed(None) } fn take_focus(&mut self) -> bool { true } + + fn layout(&mut self, size: Vec2) { + self.scrollbase.set_heights(size.y, self.items.len()); + } } diff --git a/src/view/text_view.rs b/src/view/text_view.rs index 3ad7091..e9730df 100644 --- a/src/view/text_view.rs +++ b/src/view/text_view.rs @@ -1,18 +1,18 @@ -use std::cmp::{max,min}; +use std::cmp::max; -use color; use vec::Vec2; use view::{View,DimensionRequest,SizeRequest}; use div::*; use printer::Printer; use event::*; +use super::scroll::ScrollBase; /// A simple view showing a fixed text pub struct TextView { content: String, rows: Vec, - start_line: usize, - view_height: usize, + + scrollbase: ScrollBase, } // Subset of the main content representing a row on the display. @@ -54,9 +54,8 @@ impl TextView { let content = strip_last_newline(content); TextView { content: content.to_string(), - start_line: 0, rows: Vec::new(), - view_height: 0, + scrollbase: ScrollBase::new(), } } @@ -183,36 +182,24 @@ impl <'a> Iterator for LinesIterator<'a> { impl View for TextView { fn draw(&mut self, printer: &Printer) { - // We don't have a focused view - for (i,line) in self.rows.iter().skip(self.start_line).map(|row| &self.content[row.start..row.end]).enumerate() { - printer.print((0,i), line); - } - if self.view_height < self.rows.len() { - // We directly compute the size of the scrollbar (this allow use to avoid using floats). - // (ratio) * max_height - // Where ratio is ({start or end} / content.height) - let start = self.view_height * self.start_line / self.rows.len(); - let end = self.view_height * (self.start_line + self.view_height) / self.rows.len(); - printer.with_color( - if printer.focused { color::HIGHLIGHT } else { color::HIGHLIGHT_INACTIVE }, - |printer| { - printer.print_vline((printer.size.x-1, start), end-start, ' ' as u64); - }); - } + self.scrollbase.draw(printer, |printer, i| { + let row = &self.rows[i]; + printer.print((0,0), &self.content[row.start..row.end]); + }); } fn on_event(&mut self, event: Event) -> EventResult { - if self.view_height >= self.rows.len() { + if !self.scrollbase.scrollable() { return EventResult::Ignored; } match event { - Event::KeyEvent(Key::Home) => self.start_line = 0, - Event::KeyEvent(Key::End) => self.start_line = self.rows.len() - self.view_height, - Event::KeyEvent(Key::Up) if self.start_line > 0 => self.start_line -= 1, - Event::KeyEvent(Key::Down) if self.start_line+self.view_height < self.rows.len() => self.start_line += 1, - Event::KeyEvent(Key::PageDown) => self.start_line = min(self.start_line+10, self.rows.len()-self.view_height), - Event::KeyEvent(Key::PageUp) => self.start_line -= min(self.start_line, 10), + Event::KeyEvent(Key::Home) => self.scrollbase.scroll_top(), + Event::KeyEvent(Key::End) => self.scrollbase.scroll_bottom(), + Event::KeyEvent(Key::Up) if self.scrollbase.can_scroll_up() => self.scrollbase.scroll_up(1), + Event::KeyEvent(Key::Down) if self.scrollbase.can_scroll_down() => self.scrollbase.scroll_down(1), + Event::KeyEvent(Key::PageDown) => self.scrollbase.scroll_down(10), + Event::KeyEvent(Key::PageUp) => self.scrollbase.scroll_up(10), _ => return EventResult::Ignored, } @@ -248,18 +235,15 @@ impl View for TextView { } fn take_focus(&mut self) -> bool { - self.view_height < self.rows.len() + self.scrollbase.scrollable() } fn layout(&mut self, size: Vec2) { // Compute the text rows. - self.view_height = size.y; - self.rows = LinesIterator::new(&self.content, size.x ).collect(); + self.rows = LinesIterator::new(&self.content, size.x).collect(); if self.rows.len() > size.y { self.rows = LinesIterator::new(&self.content, size.x - 1).collect(); - self.start_line = min(self.start_line, self.rows.len() - size.y); - } else { - self.start_line = 0; } + self.scrollbase.set_heights(size.y, self.rows.len()); } }