Add proper select popup placement

Added a popup select to the `list_view` example
This commit is contained in:
Alexandre Bury 2016-07-20 00:30:00 -07:00
parent 8b6022a398
commit 64d176ffa5
5 changed files with 118 additions and 26 deletions

View File

@ -2,30 +2,44 @@ extern crate cursive;
use cursive::Cursive; use cursive::Cursive;
use cursive::With; use cursive::With;
use cursive::view::{ListView, Checkbox, Dialog, EditView, TextView, LinearLayout}; use cursive::view::{Checkbox, Dialog, EditView, LinearLayout, ListView,
TextView, SelectView};
fn main() { fn main() {
let mut siv = Cursive::new(); let mut siv = Cursive::new();
siv.add_layer(Dialog::new(ListView::new() siv.add_layer(Dialog::new(ListView::new()
.child("Name", EditView::new().min_length(10)) .child("Name", EditView::new().min_length(10))
.child("Email", LinearLayout::horizontal() .child("Email",
.child(EditView::new().min_length(15).disabled().with_id("email1")) LinearLayout::horizontal()
.child(EditView::new()
.min_length(15)
.disabled()
.with_id("email1"))
.child(TextView::new("@")) .child(TextView::new("@"))
.child(EditView::new().min_length(10).disabled().with_id("email2"))) .child(EditView::new()
.child("Receive spam?", Checkbox::new().on_change(|s, checked| { .min_length(10)
.disabled()
.with_id("email2")))
.child("Receive spam?",
Checkbox::new().on_change(|s, checked| {
for name in &["email1", "email2"] { for name in &["email1", "email2"] {
let view: &mut EditView = s.find_id(name).unwrap(); let view: &mut EditView = s.find_id(name).unwrap();
view.set_enabled(checked); view.set_enabled(checked);
} }
})) }))
.delimiter() .delimiter()
.child("Age",
SelectView::new().popup()
.item_str("0-18")
.item_str("19-30")
.item_str("31-40")
.item_str("41+"))
.with(|list| { .with(|list| {
for i in 0..50 { for i in 0..50 {
list.add_child(&format!("Item {}", i), EditView::new()); list.add_child(&format!("Item {}", i), EditView::new());
} }
}) }))
)
.title("Please fill out this form") .title("Please fill out this form")
.button("Ok", |s| s.quit())); .button("Ok", |s| s.quit()));

View File

@ -200,6 +200,12 @@ impl Cursive {
B::set_refresh_rate(fps) 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. /// Returns a mutable reference to the currently active screen.
pub fn screen_mut(&mut self) -> &mut StackView { pub fn screen_mut(&mut self) -> &mut StackView {
let id = self.active_screen; let id = self.active_screen;

View File

@ -1,8 +1,10 @@
use std::rc::Rc; use std::rc::Rc;
use std::cmp::min;
use unicode_width::UnicodeWidthStr; use unicode_width::UnicodeWidthStr;
use Cursive; use Cursive;
use With;
use menu::{MenuItem, MenuTree}; use menu::{MenuItem, MenuTree};
use Printer; use Printer;
use view::View; 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 { fn item_width(item: &MenuItem) -> usize {
match *item { match *item {
MenuItem::Delimiter => 1, MenuItem::Delimiter => 1,

View File

@ -7,6 +7,7 @@ use menu::MenuTree;
use view::MenuPopup; use view::MenuPopup;
use With; use With;
use direction::Direction; use direction::Direction;
use view::position::Position;
use view::{IdView, View}; use view::{IdView, View};
use align::{Align, HAlign, VAlign}; use align::{Align, HAlign, VAlign};
use view::scroll::ScrollBase; use view::scroll::ScrollBase;
@ -45,6 +46,10 @@ pub struct SelectView<T = String> {
align: Align, align: Align,
// `true` if we show a one-line view, with popup on selection. // `true` if we show a one-line view, with popup on selection.
popup: bool, 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<Vec2>,
last_size: Vec2,
} }
impl<T: 'static> SelectView<T> { impl<T: 'static> SelectView<T> {
@ -58,6 +63,8 @@ impl<T: 'static> SelectView<T> {
select_cb: None, select_cb: None,
align: Align::top_left(), align: Align::top_left(),
popup: false, popup: false,
last_offset: Cell::new(Vec2::zero()),
last_size: Vec2::zero(),
} }
} }
@ -210,6 +217,8 @@ impl SelectView<String> {
impl<T: 'static> View for SelectView<T> { impl<T: 'static> View for SelectView<T> {
fn draw(&self, printer: &Printer) { fn draw(&self, printer: &Printer) {
self.last_offset.set(printer.offset);
if self.popup { if self.popup {
let style = if !self.enabled { let style = if !self.enabled {
ColorStyle::Secondary ColorStyle::Secondary
@ -218,15 +227,15 @@ impl<T: 'static> View for SelectView<T> {
} else { } else {
ColorStyle::Highlight ColorStyle::Highlight
}; };
let x = printer.size.x - 1; let x = printer.size.x;
printer.with_color(style, |printer| { printer.with_color(style, |printer| {
// Prepare the entire background // Prepare the entire background
printer.print_hline((0, 0), x, " "); printer.print_hline((1, 0), x - 1, " ");
// Draw the borders // Draw the borders
printer.print((0, 0), "<"); printer.print((0, 0), "<");
printer.print((x, 0), ">"); printer.print((x-1, 0), ">");
let label = &self.items[self.focus()].label; let label = &self.items[self.focus()].label;
@ -287,6 +296,8 @@ impl<T: 'static> View for SelectView<T> {
if self.popup { if self.popup {
match event { match event {
Event::Key(Key::Enter) => { Event::Key(Key::Enter) => {
// Build a shallow menu tree to mimick the items array.
// TODO: cache it?
let mut tree = MenuTree::new(); let mut tree = MenuTree::new();
for (i, item) in self.items.iter().enumerate() { for (i, item) in self.items.iter().enumerate() {
let focus = self.focus.clone(); let focus = self.focus.clone();
@ -299,10 +310,38 @@ impl<T: 'static> View for SelectView<T> {
} }
}); });
} }
// Let's keep the tree around,
// the callback will want to use it.
let tree = Rc::new(tree); 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| { EventResult::with_cb(move |s| {
// The callback will want to work with a fresh Rc
let tree = tree.clone(); 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, _ => EventResult::Ignored,
@ -354,6 +393,8 @@ impl<T: 'static> View for SelectView<T> {
} }
fn layout(&mut self, size: Vec2) { fn layout(&mut self, size: Vec2) {
self.last_size = size;
if !self.popup { if !self.popup {
self.scrollbase.set_heights(size.y, self.items.len()); self.scrollbase.set_heights(size.y, self.items.len());
} }

View File

@ -12,6 +12,7 @@ use theme::ColorStyle;
/// Only the top-most view is active and can receive input. /// Only the top-most view is active and can receive input.
pub struct StackView { pub struct StackView {
layers: Vec<Layer>, layers: Vec<Layer>,
last_size: Vec2,
} }
struct Layer { struct Layer {
@ -27,7 +28,10 @@ new_default!(StackView);
impl StackView { impl StackView {
/// Creates a new empty StackView /// Creates a new empty StackView
pub fn new() -> Self { 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. /// Adds new view on top of the stack in the center of the screen.
@ -54,6 +58,17 @@ impl StackView {
self.layers.pop(); self.layers.pop();
::B::clear(); ::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 { impl View for StackView {
@ -82,6 +97,8 @@ impl View for StackView {
} }
fn layout(&mut self, size: Vec2) { fn layout(&mut self, size: Vec2) {
self.last_size = size;
// The call has been made, we can't ask for more space anymore. // The call has been made, we can't ask for more space anymore.
// Let's make do with what we have. // Let's make do with what we have.