From 64d176ffa5da9840bc70f5b036fb17fdac4db3ce Mon Sep 17 00:00:00 2001 From: Alexandre Bury Date: Wed, 20 Jul 2016 00:30:00 -0700 Subject: [PATCH] Add proper select popup placement Added a popup select to the `list_view` example --- examples/list_view.rs | 56 +++++++++++++++++++++++++---------------- src/lib.rs | 6 +++++ src/view/menu_popup.rs | 14 +++++++++++ src/view/select_view.rs | 49 +++++++++++++++++++++++++++++++++--- src/view/stack_view.rs | 19 +++++++++++++- 5 files changed, 118 insertions(+), 26 deletions(-) diff --git a/examples/list_view.rs b/examples/list_view.rs index cd9e32a..28527da 100644 --- a/examples/list_view.rs +++ b/examples/list_view.rs @@ -2,32 +2,46 @@ extern crate cursive; use cursive::Cursive; use cursive::With; -use cursive::view::{ListView, Checkbox, Dialog, EditView, TextView, LinearLayout}; +use cursive::view::{Checkbox, Dialog, EditView, LinearLayout, ListView, + TextView, SelectView}; fn main() { let mut siv = Cursive::new(); siv.add_layer(Dialog::new(ListView::new() - .child("Name", EditView::new().min_length(10)) - .child("Email", LinearLayout::horizontal() - .child(EditView::new().min_length(15).disabled().with_id("email1")) - .child(TextView::new("@")) - .child(EditView::new().min_length(10).disabled().with_id("email2"))) - .child("Receive spam?", Checkbox::new().on_change(|s, checked| { - for name in &["email1", "email2"] { - let view: &mut EditView = s.find_id(name).unwrap(); - view.set_enabled(checked); - } - })) - .delimiter() - .with(|list| { - for i in 0..50 { - list.add_child(&format!("Item {}", i), EditView::new()); - } - }) - ) - .title("Please fill out this form") - .button("Ok", |s| s.quit())); + .child("Name", EditView::new().min_length(10)) + .child("Email", + LinearLayout::horizontal() + .child(EditView::new() + .min_length(15) + .disabled() + .with_id("email1")) + .child(TextView::new("@")) + .child(EditView::new() + .min_length(10) + .disabled() + .with_id("email2"))) + .child("Receive spam?", + Checkbox::new().on_change(|s, checked| { + for name in &["email1", "email2"] { + let view: &mut EditView = s.find_id(name).unwrap(); + view.set_enabled(checked); + } + })) + .delimiter() + .child("Age", + SelectView::new().popup() + .item_str("0-18") + .item_str("19-30") + .item_str("31-40") + .item_str("41+")) + .with(|list| { + for i in 0..50 { + list.add_child(&format!("Item {}", i), EditView::new()); + } + })) + .title("Please fill out this form") + .button("Ok", |s| s.quit())); siv.run(); } diff --git a/src/lib.rs b/src/lib.rs index ec1e43b..28c5ff7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -200,6 +200,12 @@ impl Cursive { B::set_refresh_rate(fps) } + /// Returns a reference to the currently active screen. + pub fn screen(&self) -> &StackView { + let id = self.active_screen; + &self.screens[id] + } + /// Returns a mutable reference to the currently active screen. pub fn screen_mut(&mut self) -> &mut StackView { let id = self.active_screen; diff --git a/src/view/menu_popup.rs b/src/view/menu_popup.rs index 69bea21..67f2da0 100644 --- a/src/view/menu_popup.rs +++ b/src/view/menu_popup.rs @@ -1,8 +1,10 @@ use std::rc::Rc; +use std::cmp::min; use unicode_width::UnicodeWidthStr; use Cursive; +use With; use menu::{MenuItem, MenuTree}; use Printer; use view::View; @@ -36,6 +38,18 @@ impl MenuPopup { } } + /// Sets the currently focused element. + pub fn set_focus(&mut self, focus: usize) { + self.focus = min(focus, self.menu.len()); + } + + /// Sets the currently focused element. + /// + /// Chainable variant. + pub fn focus(self, focus: usize) -> Self { + self.with(|s| s.set_focus(focus)) + } + fn item_width(item: &MenuItem) -> usize { match *item { MenuItem::Delimiter => 1, diff --git a/src/view/select_view.rs b/src/view/select_view.rs index dbf3a95..9b6d523 100644 --- a/src/view/select_view.rs +++ b/src/view/select_view.rs @@ -7,6 +7,7 @@ use menu::MenuTree; use view::MenuPopup; use With; use direction::Direction; +use view::position::Position; use view::{IdView, View}; use align::{Align, HAlign, VAlign}; use view::scroll::ScrollBase; @@ -45,6 +46,10 @@ pub struct SelectView { align: Align, // `true` if we show a one-line view, with popup on selection. popup: bool, + // We need the last offset to place the popup window + // We "cache" it during the draw, so we need interior mutability. + last_offset: Cell, + last_size: Vec2, } impl SelectView { @@ -58,6 +63,8 @@ impl SelectView { select_cb: None, align: Align::top_left(), popup: false, + last_offset: Cell::new(Vec2::zero()), + last_size: Vec2::zero(), } } @@ -210,6 +217,8 @@ impl SelectView { impl View for SelectView { fn draw(&self, printer: &Printer) { + self.last_offset.set(printer.offset); + if self.popup { let style = if !self.enabled { ColorStyle::Secondary @@ -218,15 +227,15 @@ impl View for SelectView { } else { ColorStyle::Highlight }; - let x = printer.size.x - 1; + let x = printer.size.x; printer.with_color(style, |printer| { // Prepare the entire background - printer.print_hline((0, 0), x, " "); + printer.print_hline((1, 0), x - 1, " "); // Draw the borders printer.print((0, 0), "<"); - printer.print((x, 0), ">"); + printer.print((x-1, 0), ">"); let label = &self.items[self.focus()].label; @@ -287,6 +296,8 @@ impl View for SelectView { if self.popup { match event { Event::Key(Key::Enter) => { + // Build a shallow menu tree to mimick the items array. + // TODO: cache it? let mut tree = MenuTree::new(); for (i, item) in self.items.iter().enumerate() { let focus = self.focus.clone(); @@ -299,10 +310,38 @@ impl View for SelectView { } }); } + // Let's keep the tree around, + // the callback will want to use it. let tree = Rc::new(tree); + + let focus = self.focus(); + // This is the offset for the label text. + // We'll want to show the popup so that the text matches. + // It'll be soo cool. + let text_offset = + (self.last_size.x - self.items[focus].label.len()) / 2; + // The total offset for the window is: + // * the last absolute offset at which we drew this view + // * shifted to the top of the focus (so the line matches) + // * shifted to the right of the text offset + // * shifted top-left of the border+padding of the popup + let offset = self.last_offset.get() - (0, focus) + + (text_offset, 0) - + (2, 1); + // And now, we can return the callback. EventResult::with_cb(move |s| { + // The callback will want to work with a fresh Rc let tree = tree.clone(); - s.add_layer(MenuPopup::new(tree)); + // We'll relativise the absolute position, + // So that we are locked to the parent view. + // A nice effect is that window resizes will keep both + // layers together. + let current_offset = s.screen().offset(); + let offset = offset - current_offset; + // And finally, put the view in view! + s.screen_mut() + .add_layer_at(Position::parent(offset), + MenuPopup::new(tree).focus(focus)); }) } _ => EventResult::Ignored, @@ -354,6 +393,8 @@ impl View for SelectView { } fn layout(&mut self, size: Vec2) { + self.last_size = size; + if !self.popup { self.scrollbase.set_heights(size.y, self.items.len()); } diff --git a/src/view/stack_view.rs b/src/view/stack_view.rs index dac9570..8e19da4 100644 --- a/src/view/stack_view.rs +++ b/src/view/stack_view.rs @@ -12,6 +12,7 @@ use theme::ColorStyle; /// Only the top-most view is active and can receive input. pub struct StackView { layers: Vec, + last_size: Vec2, } struct Layer { @@ -27,7 +28,10 @@ new_default!(StackView); impl StackView { /// Creates a new empty StackView pub fn new() -> Self { - StackView { layers: Vec::new() } + StackView { + layers: Vec::new(), + last_size: Vec2::zero(), + } } /// Adds new view on top of the stack in the center of the screen. @@ -54,6 +58,17 @@ impl StackView { self.layers.pop(); ::B::clear(); } + + /// Computes the offset of the current top view. + pub fn offset(&self) -> Vec2 { + let mut previous = Vec2::zero(); + for layer in &self.layers { + let offset = layer.position + .compute_offset(layer.size, self.last_size, previous); + previous = offset; + } + previous + } } impl View for StackView { @@ -82,6 +97,8 @@ impl View for StackView { } fn layout(&mut self, size: Vec2) { + self.last_size = size; + // The call has been made, we can't ask for more space anymore. // Let's make do with what we have.