Merge branch 'mouse.2'

This commit is contained in:
Alexandre Bury 2017-10-13 11:26:48 -07:00
commit 8ce817741e
70 changed files with 3694 additions and 2047 deletions

View File

@ -51,7 +51,7 @@ version = "0.11"
[dependencies.termion]
optional = true
version = "1.4.0"
version = "1.5.0"
[dev-dependencies]
rand = "0.3"

View File

@ -1,8 +1,10 @@
fn main() {
extern crate skeptic;
skeptic::generate_doc_tests(&["Readme.md",
"doc/tutorial_1.md",
"doc/tutorial_2.md",
"doc/tutorial_3.md" ]);
skeptic::generate_doc_tests(&[
"Readme.md",
"doc/tutorial_1.md",
"doc/tutorial_2.md",
"doc/tutorial_3.md",
]);
}

View File

@ -1,7 +1,7 @@
extern crate cursive;
use cursive::{Cursive, Printer};
use cursive::theme::{ColorStyle, Color};
use cursive::theme::{Color, ColorStyle};
use cursive::view::Boxable;
use cursive::views::Canvas;
@ -16,16 +16,19 @@ fn main() {
}
fn front_color(x: u8, y: u8, x_max: u8, y_max: u8) -> Color {
Color::Rgb(x * (255 / x_max),
y * (255 / y_max),
(x + 2 * y) * (255 / (x_max + 2 * y_max)))
Color::Rgb(
x * (255 / x_max),
y * (255 / y_max),
(x + 2 * y) * (255 / (x_max + 2 * y_max)),
)
}
fn back_color(x: u8, y: u8, x_max: u8, y_max: u8) -> Color {
Color::Rgb(128 + (2 * y_max + x - 2 * y) * (128 / (x_max + 2 * y_max)),
255 - y * (255 / y_max),
255 - x * (255 / x_max))
Color::Rgb(
128 + (2 * y_max + x - 2 * y) * (128 / (x_max + 2 * y_max)),
255 - y * (255 / y_max),
255 - x * (255 / x_max),
)
}
fn draw(_: &(), p: &Printer) {
@ -39,7 +42,9 @@ fn draw(_: &(), p: &Printer) {
back: back_color(x, y, x_max, y_max),
};
p.with_color(style, |printer| { printer.print((x, y), "+"); });
p.with_color(style, |printer| {
printer.print((x, y), "+");
});
}
}
}

View File

@ -8,9 +8,11 @@ fn main() {
let mut siv = Cursive::new();
// Creates a dialog with a single "Quit" button
siv.add_layer(Dialog::around(TextView::new("Hello Dialog!"))
.title("Cursive")
.button("Quit", |s| s.quit()));
siv.add_layer(
Dialog::around(TextView::new("Hello Dialog!"))
.title("Cursive")
.button("Quit", |s| s.quit()),
);
// Starts the event loop.
siv.run();

View File

@ -10,19 +10,24 @@ fn main() {
// Create a dialog with an edit text and a button.
// The user can either hit the <Ok> button,
// or press Enter on the edit text.
siv.add_layer(Dialog::new()
.title("Enter your name")
.padding((1, 1, 1, 0))
.content(EditView::new()
.on_submit(show_popup)
.with_id("name")
.fixed_width(20))
.button("Ok", |s| {
let name =
s.call_on_id("name", |view: &mut EditView| view.get_content())
.unwrap();
show_popup(s, &name);
}));
siv.add_layer(
Dialog::new()
.title("Enter your name")
.padding((1, 1, 1, 0))
.content(
EditView::new()
.on_submit(show_popup)
.with_id("name")
.fixed_width(20),
)
.button("Ok", |s| {
let name = s.call_on_id(
"name",
|view: &mut EditView| view.get_content(),
).unwrap();
show_popup(s, &name);
}),
);
siv.run();
}
@ -33,7 +38,9 @@ fn show_popup(s: &mut Cursive, name: &str) {
} else {
let content = format!("Hello {}!", name);
s.pop_layer();
s.add_layer(Dialog::around(TextView::new(content))
.button("Quit", |s| s.quit()));
s.add_layer(
Dialog::around(TextView::new(content))
.button("Quit", |s| s.quit()),
);
}
}

View File

@ -9,8 +9,10 @@ fn main() {
// We can quit by pressing `q`
siv.add_global_callback('q', Cursive::quit);
siv.add_layer(TextView::new("Hello World!\n\
Press q to quit the application."));
siv.add_layer(TextView::new(
"Hello World!\n\
Press q to quit the application.",
));
siv.run();
}

View File

@ -1,13 +1,13 @@
extern crate cursive;
use cursive::{Cursive, Printer};
use cursive::traits::*;
use cursive::event::{Event, EventResult};
use cursive::traits::*;
fn main() {
let mut siv = Cursive::new();
siv.add_layer(KeyCodeView::new(10).fixed_size((70, 10)));
siv.add_layer(KeyCodeView::new(10).full_width().fixed_height(10));
siv.run();
}

View File

@ -1,9 +1,9 @@
extern crate cursive;
use cursive::Cursive;
use cursive::views::{Dialog, LinearLayout, TextView};
use cursive::align::HAlign;
use cursive::traits::*;
use cursive::views::{Dialog, LinearLayout, TextView};
fn main() {
let mut siv = Cursive::new();
@ -14,16 +14,19 @@ fn main() {
has a fixed width, and the title is centered horizontally.";
// We'll create a dialog with a TextView serving as a title
siv.add_layer(Dialog::around(LinearLayout::vertical()
siv.add_layer(
Dialog::around(
LinearLayout::vertical()
.child(TextView::new("Title").h_align(HAlign::Center))
// Box the textview, so it doesn't get too wide.
// A 0 height value means it will be unconstrained.
.child(TextView::new(text).scrollable(false).fixed_width(30))
.child(TextView::new(text).fixed_width(30))
.child(TextView::new(text).fixed_width(30))
.child(TextView::new(text).fixed_width(30)))
.button("Quit", |s| s.quit())
.h_align(HAlign::Center));
.child(TextView::new(text).fixed_width(30)),
).button("Quit", |s| s.quit())
.h_align(HAlign::Center),
);
siv.run();
}

View File

@ -8,44 +8,61 @@ use cursive::views::{Checkbox, Dialog, EditView, LinearLayout, ListView,
fn main() {
let mut siv = Cursive::new();
siv.add_layer(Dialog::new()
.title("Please fill out this form")
.button("Ok", |s| s.quit())
.content(ListView::new()
.child("Name", EditView::new().fixed_width(10))
.child("Receive spam?",
Checkbox::new()
.on_change(|s, checked| for name in &["email1",
"email2"] {
s.call_on_id(name, |view: &mut EditView| {
view.set_enabled(checked)
});
if checked {
s.focus_id("email1").unwrap();
}
}))
.child("Email",
LinearLayout::horizontal()
.child(EditView::new()
.disabled()
.with_id("email1")
.fixed_width(15))
.child(TextView::new("@"))
.child(EditView::new()
.disabled()
.with_id("email2")
.fixed_width(10)))
.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());
})));
siv.add_layer(
Dialog::new()
.title("Please fill out this form")
.button("Ok", |s| s.quit())
.content(
ListView::new()
.child("Name", EditView::new().fixed_width(10))
.child(
"Receive spam?",
Checkbox::new().on_change(
|s, checked| for name in &["email1", "email2"] {
s.call_on_id(name, |view: &mut EditView| {
view.set_enabled(checked)
});
if checked {
s.focus_id("email1").unwrap();
}
},
),
)
.child(
"Email",
LinearLayout::horizontal()
.child(
EditView::new()
.disabled()
.with_id("email1")
.fixed_width(15),
)
.child(TextView::new("@"))
.child(
EditView::new()
.disabled()
.with_id("email2")
.fixed_width(10),
),
)
.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(),
);
}),
),
);
siv.run();
}

View File

@ -4,7 +4,6 @@ use cursive::{Cursive, Printer};
use cursive::traits::*;
use cursive::vec::Vec2;
use std::collections::VecDeque;
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
@ -21,7 +20,9 @@ fn main() {
let (tx, rx) = mpsc::channel();
// Generate data in a separate thread.
thread::spawn(move || { generate_logs(&tx); });
thread::spawn(move || {
generate_logs(&tx);
});
// And sets the view to read from the other end of the channel.
siv.add_layer(BufferView::new(200, rx).full_screen());
@ -82,11 +83,9 @@ impl View for BufferView {
fn draw(&self, printer: &Printer) {
// Print the end of the buffer
for (i, line) in self.buffer
.iter()
.rev()
.take(printer.size.y)
.enumerate() {
for (i, line) in
self.buffer.iter().rev().take(printer.size.y).enumerate()
{
printer.print((0, printer.size.y - 1 - i), line);
}
}

View File

@ -1,9 +1,9 @@
extern crate cursive;
use cursive::Cursive;
use cursive::align::HAlign;
use cursive::view::Boxable;
use cursive::views::{Dialog, TextView};
use cursive::align::HAlign;
fn main() {
// Read some long text from a file.
@ -16,13 +16,17 @@ fn main() {
// The text is too long to fit on a line, so the view will wrap lines,
// and will adapt to the terminal size.
siv.add_fullscreen_layer(Dialog::around(TextView::new(content))
.h_align(HAlign::Center)
.button("Quit", |s| s.quit())
.full_screen());
siv.add_fullscreen_layer(
Dialog::around(TextView::new(content))
.h_align(HAlign::Center)
.button("Quit", |s| s.quit())
.full_screen(),
);
// Show a popup on top of the view.
siv.add_layer(Dialog::info("Try resizing the terminal!\n(Press 'q' to \
quit when you're done.)"));
siv.add_layer(Dialog::info(
"Try resizing the terminal!\n(Press 'q' to \
quit when you're done.)",
));
siv.run();
}

View File

@ -5,11 +5,9 @@ use cursive::event::Key;
use cursive::menu::MenuTree;
use cursive::traits::*;
use cursive::views::Dialog;
use std::sync::atomic::{Ordering, AtomicUsize};
use std::sync::atomic::{AtomicUsize, Ordering};
fn main() {
let mut siv = Cursive::new();
// We'll use a counter to name new files.

View File

@ -1,12 +1,11 @@
extern crate cursive;
use cursive::Cursive;
use cursive::views::{Dialog, OnEventView, TextView};
use cursive::view::{Offset, Position};
use cursive::traits::*;
use cursive::view::{Offset, Position};
use cursive::views::{Dialog, OnEventView, TextView};
fn show_popup(siv: &mut Cursive) {
// Let's center the popup horizontally, but offset it down a few rows
siv.screen_mut()
.add_layer_at(Position::new(Offset::Center, Offset::Parent(3)),
@ -19,7 +18,6 @@ fn show_popup(siv: &mut Cursive) {
});
})
.dismiss_button("Ok"));
}
fn main() {
@ -33,8 +31,10 @@ fn main() {
// Let's wrap the view to give it a recognizable ID, so we can look for it.
// We add the P callback on the textview only (and not globally),
// so that we can't call it when the popup is already visible.
siv.add_layer(OnEventView::new(TextView::new(content).with_id("text"))
.on_event('p', |s| show_popup(s)));
siv.add_layer(
OnEventView::new(TextView::new(content).with_id("text"))
.on_event('p', |s| show_popup(s)),
);
siv.run();

View File

@ -1,25 +1,25 @@
extern crate cursive;
extern crate rand;
use rand::Rng;
use cursive::Cursive;
use cursive::traits::*;
use cursive::views::{Button, Dialog, LinearLayout, ProgressBar, TextView};
use cursive::views::Counter;
use cursive::traits::*;
use std::thread;
use rand::Rng;
use std::cmp::min;
use std::thread;
use std::time::Duration;
fn main() {
let mut siv = Cursive::new();
// We'll start slowly with a simple start button...
siv.add_layer(Dialog::new()
.title("Progress bar example")
.padding((0, 0, 1, 1))
.content(Button::new("Start", phase_1)));
siv.add_layer(
Dialog::new()
.title("Progress bar example")
.padding((0, 0, 1, 1))
.content(Button::new("Start", phase_1)),
);
// Auto-refresh is currently required for animated views
siv.set_fps(30);
@ -46,25 +46,29 @@ fn phase_1(s: &mut Cursive) {
let cb = s.cb_sink().clone();
s.pop_layer();
s.add_layer(Dialog::around(ProgressBar::new()
.range(0, n_max)
.with_task(move |counter| {
// This closure will be called in a separate thread.
fake_load(n_max, &counter);
s.add_layer(Dialog::around(
ProgressBar::new()
.range(0, n_max)
.with_task(move |counter| {
// This closure will be called in a separate thread.
fake_load(n_max, &counter);
// When we're done, send a callback through the channel
cb.send(Box::new(coffee_break)).unwrap();
})
.full_width()));
// When we're done, send a callback through the channel
cb.send(Box::new(coffee_break)).unwrap();
})
.full_width(),
));
}
fn coffee_break(s: &mut Cursive) {
// A little break before things get serious.
s.pop_layer();
s.add_layer(Dialog::new()
.title("Preparation complete")
.content(TextView::new("Now, the real deal!").center())
.button("Again??", phase_2));
s.add_layer(
Dialog::new()
.title("Preparation complete")
.content(TextView::new("Now, the real deal!").center())
.button("Again??", phase_2),
);
}
fn phase_2(s: &mut Cursive) {
@ -75,8 +79,9 @@ fn phase_2(s: &mut Cursive) {
// Each task will have its own shiny counter
let counters: Vec<_> = (0..n_bars).map(|_| Counter::new(0)).collect();
// To make things more interesting, we'll give a random speed to each bar
let speeds: Vec<_> =
(0..n_bars).map(|_| rand::thread_rng().gen_range(50, 150)).collect();
let speeds: Vec<_> = (0..n_bars)
.map(|_| rand::thread_rng().gen_range(50, 150))
.collect();
let n_max = 100_000;
let cb = s.cb_sink().clone();
@ -84,9 +89,7 @@ fn phase_2(s: &mut Cursive) {
// Let's prepare the progress bars...
let mut linear = LinearLayout::vertical();
for c in &counters {
linear.add_child(ProgressBar::new()
.max(n_max)
.with_value(c.clone()));
linear.add_child(ProgressBar::new().max(n_max).with_value(c.clone()));
}
s.pop_layer();
@ -116,10 +119,15 @@ fn phase_2(s: &mut Cursive) {
fn final_step(s: &mut Cursive) {
// A little break before things get serious.
s.pop_layer();
s.add_layer(Dialog::new()
.title("Report")
.content(TextView::new("Time travel was a success!\n\
We went forward a few seconds!!")
.center())
.button("That's it?", |s| s.quit()));
s.add_layer(
Dialog::new()
.title("Report")
.content(
TextView::new(
"Time travel was a success!\n\
We went forward a few seconds!!",
).center(),
)
.button("That's it?", |s| s.quit()),
);
}

View File

@ -10,7 +10,8 @@ fn main() {
let mut color_group: RadioGroup<String> = RadioGroup::new();
let mut size_group: RadioGroup<u32> = RadioGroup::new();
siv.add_layer(Dialog::new()
siv.add_layer(
Dialog::new()
.title("Make your selection")
// We'll have two columns side-by-side
.content(LinearLayout::horizontal()
@ -37,7 +38,8 @@ fn main() {
color,
size))
.button("Ok", |s| s.quit()));
}));
}),
);
siv.run();
}

View File

@ -2,19 +2,22 @@ extern crate cursive;
use cursive::Cursive;
use cursive::view::{Boxable, Identifiable};
use cursive::views::{LinearLayout, EditView, TextView, Dialog};
use cursive::views::{Dialog, EditView, LinearLayout, TextView};
fn main() {
let mut siv = Cursive::new();
// Create a dialog with 2 edit fields, and a text view.
// The text view indicates when the 2 fields content match.
siv.add_layer(Dialog::around(LinearLayout::vertical()
.child(EditView::new().on_edit(on_edit).with_id("1"))
.child(EditView::new().on_edit(on_edit).with_id("2"))
.child(TextView::new("match").with_id("match"))
.fixed_width(10))
.button("Quit", Cursive::quit));
siv.add_layer(
Dialog::around(
LinearLayout::vertical()
.child(EditView::new().on_edit(on_edit).with_id("1"))
.child(EditView::new().on_edit(on_edit).with_id("2"))
.child(TextView::new("match").with_id("match"))
.fixed_width(10),
).button("Quit", Cursive::quit),
);
siv.run();
}
@ -31,9 +34,7 @@ fn on_edit(siv: &mut Cursive, _content: &str, _cursor: usize) {
// Directly compare references to edit_1 and edit_2.
let matches = edit_1.get_content() == edit_2.get_content();
siv.call_on_id("match", |v: &mut TextView| v.set_content(if matches {
"match"
} else {
"no match"
}));
siv.call_on_id("match", |v: &mut TextView| {
v.set_content(if matches { "match" } else { "no match" })
});
}

View File

@ -10,19 +10,27 @@ fn main() {
// Let's add a simple slider in a dialog.
// Moving the slider will update the dialog's title.
// And pressing "Enter" will show a new dialog.
siv.add_layer(Dialog::around(SliderView::horizontal(15)
.value(7)
.on_change(|s, v| {
let title = format!("[ {} ]", v);
s.call_on_id("dialog", |view: &mut Dialog| view.set_title(title));
})
.on_enter(|s, v| {
s.pop_layer();
s.add_layer(Dialog::text(format!("Lucky number {}!", v))
.button("Ok", Cursive::quit));
}))
.title("[ 7 ]")
.with_id("dialog"));
siv.add_layer(
Dialog::around(
SliderView::horizontal(15)
.value(7)
.on_change(|s, v| {
let title = format!("[ {} ]", v);
s.call_on_id(
"dialog",
|view: &mut Dialog| view.set_title(title),
);
})
.on_enter(|s, v| {
s.pop_layer();
s.add_layer(
Dialog::text(format!("Lucky number {}!", v))
.button("Ok", Cursive::quit),
);
}),
).title("[ 7 ]")
.with_id("dialog"),
);
siv.run();
}

View File

@ -20,8 +20,10 @@ fn main() {
siv.add_global_callback('q', Cursive::quit);
siv.set_theme(theme);
siv.add_layer(TextView::new("Hello World with default terminal background color!\n\
Press q to quit the application."));
siv.add_layer(TextView::new(
"Hello World with default terminal background color!\n\
Press q to quit the application.",
));
siv.run();
}

View File

@ -1,18 +1,19 @@
extern crate cursive;
use cursive::Cursive;
use cursive::views::{Dialog, TextArea};
use cursive::traits::*;
use cursive::views::{Dialog, TextArea};
fn main() {
let mut siv = Cursive::new();
siv.add_layer(Dialog::new()
.title("Describe your issue")
.padding((1, 1, 1, 0))
.content(TextArea::new()
.with_id("text"))
.button("Ok", Cursive::quit));
siv.add_layer(
Dialog::new()
.title("Describe your issue")
.padding((1, 1, 1, 0))
.content(TextArea::new().with_id("text"))
.button("Ok", Cursive::quit),
);
siv.run();
}

View File

@ -11,10 +11,13 @@ fn main() {
// Or you can directly load it from a string for easy deployment.
// siv.load_theme(include_str!("../assets/style.toml")).unwrap();
siv.add_layer(Dialog::around(TextView::new("This application uses a \
custom theme!"))
.title("Themed dialog")
.button("Quit", |s| s.quit()));
siv.add_layer(
Dialog::around(TextView::new(
"This application uses a \
custom theme!",
)).title("Themed dialog")
.button("Quit", |s| s.quit()),
);
siv.run();
}

View File

@ -1,33 +1,37 @@
extern crate cursive;
use cursive::Cursive;
use cursive::theme::{ColorStyle, BaseColor, Color, BorderStyle};
use cursive::views::{EditView, LinearLayout, Dialog, TextView};
use cursive::theme::{BaseColor, BorderStyle, Color, ColorStyle};
use cursive::views::{Dialog, EditView, LinearLayout, TextView};
fn main() {
let mut siv = Cursive::new();
let layout = LinearLayout::vertical()
.child(TextView::new("This is a dynamic theme example!"))
.child(EditView::new().content("Woo! colors!").style(ColorStyle::Custom {
front: Color::Rgb(200, 150, 150),
back: Color::Dark(BaseColor::Blue),
}));
.child(EditView::new().content("Woo! colors!").style(
ColorStyle::Custom {
front: Color::Rgb(200, 150, 150),
back: Color::Dark(BaseColor::Blue),
},
));
siv.add_layer(Dialog::around(layout)
.button("Change", |s| {
let mut theme = s.current_theme().clone();
siv.add_layer(
Dialog::around(layout)
.button("Change", |s| {
let mut theme = s.current_theme().clone();
theme.shadow = !theme.shadow;
theme.borders = match theme.borders {
BorderStyle::Simple => BorderStyle::Outset,
BorderStyle::Outset => BorderStyle::None,
BorderStyle::None => BorderStyle::Simple,
};
theme.shadow = !theme.shadow;
theme.borders = match theme.borders {
BorderStyle::Simple => BorderStyle::Outset,
BorderStyle::Outset => BorderStyle::None,
BorderStyle::None => BorderStyle::Simple,
};
s.set_theme(theme);
})
.button("Quit", Cursive::quit));
s.set_theme(theme);
})
.button("Quit", Cursive::quit),
);
siv.run();
}

View File

@ -5,23 +5,164 @@ use self::bear_lib_terminal::geometry::Size;
use self::bear_lib_terminal::terminal::{self, state, Event as BltEvent,
KeyCode};
use backend;
use event::{Event, Key};
use std::collections::BTreeMap;
use event::{Event, Key, MouseButton, MouseEvent};
use std::collections::HashSet;
use theme::{BaseColor, Color, ColorPair, Effect};
use vec::Vec2;
enum ColorRole {
Foreground,
Background,
}
pub struct Concrete {}
pub struct Concrete {
mouse_position: Vec2,
buttons_pressed: HashSet<MouseButton>,
}
impl Concrete {
fn blt_keycode_to_ev(
&mut self, kc: KeyCode, shift: bool, ctrl: bool
) -> Event {
match kc {
KeyCode::F1 |
KeyCode::F2 |
KeyCode::F3 |
KeyCode::F4 |
KeyCode::F5 |
KeyCode::F6 |
KeyCode::F7 |
KeyCode::F8 |
KeyCode::F9 |
KeyCode::F10 |
KeyCode::F11 |
KeyCode::F12 |
KeyCode::NumEnter |
KeyCode::Enter |
KeyCode::Escape |
KeyCode::Backspace |
KeyCode::Tab |
KeyCode::Pause |
KeyCode::Insert |
KeyCode::Home |
KeyCode::PageUp |
KeyCode::Delete |
KeyCode::End |
KeyCode::PageDown |
KeyCode::Right |
KeyCode::Left |
KeyCode::Down |
KeyCode::Up => match (shift, ctrl) {
(true, true) => Event::CtrlShift(blt_keycode_to_key(kc)),
(true, false) => Event::Shift(blt_keycode_to_key(kc)),
(false, true) => Event::Ctrl(blt_keycode_to_key(kc)),
(false, false) => Event::Key(blt_keycode_to_key(kc)),
},
// TODO: mouse support
KeyCode::MouseLeft |
KeyCode::MouseRight |
KeyCode::MouseMiddle |
KeyCode::MouseFourth |
KeyCode::MouseFifth => blt_keycode_to_mouse_button(kc)
.map(|btn| {
self.buttons_pressed.insert(btn);
Event::Mouse {
event: MouseEvent::Press(btn),
position: self.mouse_position,
offset: Vec2::zero(),
}
})
.unwrap_or(Event::Unknown(vec![])),
KeyCode::A |
KeyCode::B |
KeyCode::C |
KeyCode::D |
KeyCode::E |
KeyCode::F |
KeyCode::G |
KeyCode::H |
KeyCode::I |
KeyCode::J |
KeyCode::K |
KeyCode::L |
KeyCode::M |
KeyCode::N |
KeyCode::O |
KeyCode::P |
KeyCode::Q |
KeyCode::R |
KeyCode::S |
KeyCode::T |
KeyCode::U |
KeyCode::V |
KeyCode::W |
KeyCode::X |
KeyCode::Y |
KeyCode::Z |
KeyCode::Row1 |
KeyCode::Row2 |
KeyCode::Row3 |
KeyCode::Row4 |
KeyCode::Row5 |
KeyCode::Row6 |
KeyCode::Row7 |
KeyCode::Row8 |
KeyCode::Row9 |
KeyCode::Row0 |
KeyCode::Grave |
KeyCode::Minus |
KeyCode::Equals |
KeyCode::LeftBracket |
KeyCode::RightBracket |
KeyCode::Backslash |
KeyCode::Semicolon |
KeyCode::Apostrophe |
KeyCode::Comma |
KeyCode::Period |
KeyCode::Slash |
KeyCode::Space |
KeyCode::NumDivide |
KeyCode::NumMultiply |
KeyCode::NumMinus |
KeyCode::NumPlus |
KeyCode::NumPeriod |
KeyCode::Num1 |
KeyCode::Num2 |
KeyCode::Num3 |
KeyCode::Num4 |
KeyCode::Num5 |
KeyCode::Num6 |
KeyCode::Num7 |
KeyCode::Num8 |
KeyCode::Num9 |
KeyCode::Num0 => if ctrl {
Event::CtrlChar(blt_keycode_to_char(kc, shift))
} else {
Event::Char(blt_keycode_to_char(kc, shift))
},
}
}
}
impl backend::Backend for Concrete {
fn init() -> Self {
terminal::open("Cursive", 80, 24);
terminal::set(terminal::config::Window::empty().resizeable(true));
terminal::set(vec![
terminal::config::InputFilter::Group {
group: terminal::config::InputFilterGroup::Keyboard,
both: false,
},
terminal::config::InputFilter::Group {
group: terminal::config::InputFilterGroup::Mouse,
both: true,
},
]);
Concrete {}
Concrete {
mouse_position: Vec2::zero(),
buttons_pressed: HashSet::new(),
}
}
fn finish(&mut self) {
@ -41,13 +182,11 @@ impl backend::Backend for Concrete {
// BLT itself doesn't do this kind of thing,
// we'd need the colours in our position,
// but `f()` can do whatever
Effect::Reverse => {
terminal::with_colors(
BltColor::from_rgb(0, 0, 0),
BltColor::from_rgb(255, 255, 255),
f,
)
}
Effect::Reverse => terminal::with_colors(
BltColor::from_rgb(0, 0, 0),
BltColor::from_rgb(255, 255, 255),
f,
),
}
}
@ -87,18 +226,50 @@ impl backend::Backend for Concrete {
BltEvent::Close => Event::Exit,
BltEvent::Resize { .. } => Event::WindowResize,
// TODO: mouse support
BltEvent::MouseMove { .. } => Event::Refresh,
BltEvent::MouseScroll { .. } => Event::Refresh,
BltEvent::MouseMove { x, y } => {
self.mouse_position = Vec2::new(x as usize, y as usize);
// TODO: find out if a button is pressed?
match self.buttons_pressed.iter().next() {
None => Event::Refresh,
Some(btn) => Event::Mouse {
event: MouseEvent::Hold(*btn),
position: self.mouse_position,
offset: Vec2::zero(),
},
}
}
BltEvent::MouseScroll { delta } => Event::Mouse {
event: if delta < 0 {
MouseEvent::WheelUp
} else {
MouseEvent::WheelDown
},
position: self.mouse_position,
offset: Vec2::zero(),
},
BltEvent::KeyPressed { key, ctrl, shift } => {
blt_keycode_to_ev(key, shift, ctrl)
self.blt_keycode_to_ev(key, shift, ctrl)
}
// TODO: there's no Key::Shift/Ctrl for w/e reason
BltEvent::ShiftPressed => Event::Refresh,
BltEvent::ControlPressed => Event::Refresh,
// TODO: what should we do here?
BltEvent::KeyReleased { .. } |
BltEvent::ShiftReleased |
BltEvent::ControlReleased => Event::Refresh,
BltEvent::KeyReleased { key, .. } => {
// It's probably a mouse key.
blt_keycode_to_mouse_button(key)
.map(|btn| {
self.buttons_pressed.remove(&btn);
Event::Mouse {
event: MouseEvent::Release(btn),
position: self.mouse_position,
offset: Vec2::zero(),
}
})
.unwrap_or(Event::Unknown(vec![]))
}
BltEvent::ShiftReleased | BltEvent::ControlReleased => {
Event::Refresh
}
}
} else {
Event::Refresh
@ -138,111 +309,252 @@ fn colour_to_blt_colour(clr: Color, role: ColorRole) -> BltColor {
Color::Light(BaseColor::White) => (255, 255, 255),
Color::Rgb(r, g, b) => (r, g, b),
Color::RgbLowRes(r, g, b) => {
(
(r as f32 / 5.0 * 255.0) as u8,
(g as f32 / 5.0 * 255.0) as u8,
(b as f32 / 5.0 * 255.0) as u8,
)
}
Color::RgbLowRes(r, g, b) => (
(r as f32 / 5.0 * 255.0) as u8,
(g as f32 / 5.0 * 255.0) as u8,
(b as f32 / 5.0 * 255.0) as u8,
),
};
BltColor::from_rgb(r, g, b)
}
fn blt_keycode_to_ev(kc: KeyCode, shift: bool, ctrl: bool) -> Event {
match kc {
KeyCode::F1 | KeyCode::F2 | KeyCode::F3 | KeyCode::F4 |
KeyCode::F5 | KeyCode::F6 | KeyCode::F7 | KeyCode::F8 |
KeyCode::F9 | KeyCode::F10 | KeyCode::F11 | KeyCode::F12 |
KeyCode::NumEnter | KeyCode::Enter | KeyCode::Escape |
KeyCode::Backspace | KeyCode::Tab | KeyCode::Pause |
KeyCode::Insert | KeyCode::Home | KeyCode::PageUp |
KeyCode::Delete | KeyCode::End | KeyCode::PageDown |
KeyCode::Right | KeyCode::Left | KeyCode::Down | KeyCode::Up => {
match (shift, ctrl) {
(true, true) => Event::CtrlShift(blt_keycode_to_key(kc)),
(true, false) => Event::Shift(blt_keycode_to_key(kc)),
(false, true) => Event::Ctrl(blt_keycode_to_key(kc)),
(false, false) => Event::Key(blt_keycode_to_key(kc)),
}
}
// TODO: mouse support
KeyCode::MouseLeft | KeyCode::MouseRight | KeyCode::MouseMiddle |
KeyCode::MouseFourth | KeyCode::MouseFifth => Event::Refresh,
KeyCode::A | KeyCode::B | KeyCode::C | KeyCode::D | KeyCode::E |
KeyCode::F | KeyCode::G | KeyCode::H | KeyCode::I | KeyCode::J |
KeyCode::K | KeyCode::L | KeyCode::M | KeyCode::N | KeyCode::O |
KeyCode::P | KeyCode::Q | KeyCode::R | KeyCode::S | KeyCode::T |
KeyCode::U | KeyCode::V | KeyCode::W | KeyCode::X | KeyCode::Y |
KeyCode::Z | KeyCode::Row1 | KeyCode::Row2 | KeyCode::Row3 |
KeyCode::Row4 | KeyCode::Row5 | KeyCode::Row6 | KeyCode::Row7 |
KeyCode::Row8 | KeyCode::Row9 | KeyCode::Row0 | KeyCode::Grave |
KeyCode::Minus | KeyCode::Equals | KeyCode::LeftBracket |
KeyCode::RightBracket | KeyCode::Backslash | KeyCode::Semicolon |
KeyCode::Apostrophe | KeyCode::Comma | KeyCode::Period |
KeyCode::Slash | KeyCode::Space | KeyCode::NumDivide |
KeyCode::NumMultiply | KeyCode::NumMinus | KeyCode::NumPlus |
KeyCode::NumPeriod | KeyCode::Num1 | KeyCode::Num2 |
KeyCode::Num3 | KeyCode::Num4 | KeyCode::Num5 | KeyCode::Num6 |
KeyCode::Num7 | KeyCode::Num8 | KeyCode::Num9 | KeyCode::Num0 => {
if ctrl {
Event::CtrlChar(blt_keycode_to_char(kc, shift))
} else {
Event::Char(blt_keycode_to_char(kc, shift))
}
}
}
}
fn blt_keycode_to_char(kc: KeyCode, shift: bool) -> char {
match kc {
KeyCode::A => if shift { 'A' } else { 'a' },
KeyCode::B => if shift { 'B' } else { 'b' },
KeyCode::C => if shift { 'C' } else { 'c' },
KeyCode::D => if shift { 'D' } else { 'd' },
KeyCode::E => if shift { 'E' } else { 'e' },
KeyCode::F => if shift { 'F' } else { 'f' },
KeyCode::G => if shift { 'G' } else { 'g' },
KeyCode::H => if shift { 'H' } else { 'h' },
KeyCode::I => if shift { 'I' } else { 'i' },
KeyCode::J => if shift { 'J' } else { 'j' },
KeyCode::K => if shift { 'K' } else { 'k' },
KeyCode::L => if shift { 'L' } else { 'l' },
KeyCode::M => if shift { 'M' } else { 'm' },
KeyCode::N => if shift { 'N' } else { 'n' },
KeyCode::O => if shift { 'O' } else { 'o' },
KeyCode::P => if shift { 'P' } else { 'p' },
KeyCode::Q => if shift { 'Q' } else { 'q' },
KeyCode::R => if shift { 'R' } else { 'r' },
KeyCode::S => if shift { 'S' } else { 's' },
KeyCode::T => if shift { 'T' } else { 't' },
KeyCode::U => if shift { 'U' } else { 'u' },
KeyCode::V => if shift { 'V' } else { 'v' },
KeyCode::W => if shift { 'W' } else { 'w' },
KeyCode::X => if shift { 'X' } else { 'x' },
KeyCode::Y => if shift { 'Y' } else { 'y' },
KeyCode::Z => if shift { 'Z' } else { 'z' },
KeyCode::Row1 => if shift { '!' } else { '1' },
KeyCode::Row2 => if shift { '@' } else { '2' },
KeyCode::Row3 => if shift { '#' } else { '3' },
KeyCode::Row4 => if shift { '$' } else { '4' },
KeyCode::Row5 => if shift { '%' } else { '5' },
KeyCode::Row6 => if shift { '^' } else { '6' },
KeyCode::Row7 => if shift { '&' } else { '7' },
KeyCode::Row8 => if shift { '*' } else { '8' },
KeyCode::Row9 => if shift { '(' } else { '9' },
KeyCode::Row0 => if shift { ')' } else { '0' },
KeyCode::Grave => if shift { '~' } else { '`' },
KeyCode::Minus => if shift { '_' } else { '-' },
KeyCode::Equals => if shift { '+' } else { '=' },
KeyCode::LeftBracket => if shift { '{' } else { '[' },
KeyCode::RightBracket => if shift { '}' } else { ']' },
KeyCode::Backslash => if shift { '|' } else { '\\' },
KeyCode::Semicolon => if shift { ':' } else { ';' },
KeyCode::Apostrophe => if shift { '"' } else { '\'' },
KeyCode::Comma => if shift { '<' } else { ',' },
KeyCode::Period => if shift { '>' } else { '.' },
KeyCode::Slash => if shift { '?' } else { '/' },
KeyCode::A => if shift {
'A'
} else {
'a'
},
KeyCode::B => if shift {
'B'
} else {
'b'
},
KeyCode::C => if shift {
'C'
} else {
'c'
},
KeyCode::D => if shift {
'D'
} else {
'd'
},
KeyCode::E => if shift {
'E'
} else {
'e'
},
KeyCode::F => if shift {
'F'
} else {
'f'
},
KeyCode::G => if shift {
'G'
} else {
'g'
},
KeyCode::H => if shift {
'H'
} else {
'h'
},
KeyCode::I => if shift {
'I'
} else {
'i'
},
KeyCode::J => if shift {
'J'
} else {
'j'
},
KeyCode::K => if shift {
'K'
} else {
'k'
},
KeyCode::L => if shift {
'L'
} else {
'l'
},
KeyCode::M => if shift {
'M'
} else {
'm'
},
KeyCode::N => if shift {
'N'
} else {
'n'
},
KeyCode::O => if shift {
'O'
} else {
'o'
},
KeyCode::P => if shift {
'P'
} else {
'p'
},
KeyCode::Q => if shift {
'Q'
} else {
'q'
},
KeyCode::R => if shift {
'R'
} else {
'r'
},
KeyCode::S => if shift {
'S'
} else {
's'
},
KeyCode::T => if shift {
'T'
} else {
't'
},
KeyCode::U => if shift {
'U'
} else {
'u'
},
KeyCode::V => if shift {
'V'
} else {
'v'
},
KeyCode::W => if shift {
'W'
} else {
'w'
},
KeyCode::X => if shift {
'X'
} else {
'x'
},
KeyCode::Y => if shift {
'Y'
} else {
'y'
},
KeyCode::Z => if shift {
'Z'
} else {
'z'
},
KeyCode::Row1 => if shift {
'!'
} else {
'1'
},
KeyCode::Row2 => if shift {
'@'
} else {
'2'
},
KeyCode::Row3 => if shift {
'#'
} else {
'3'
},
KeyCode::Row4 => if shift {
'$'
} else {
'4'
},
KeyCode::Row5 => if shift {
'%'
} else {
'5'
},
KeyCode::Row6 => if shift {
'^'
} else {
'6'
},
KeyCode::Row7 => if shift {
'&'
} else {
'7'
},
KeyCode::Row8 => if shift {
'*'
} else {
'8'
},
KeyCode::Row9 => if shift {
'('
} else {
'9'
},
KeyCode::Row0 => if shift {
')'
} else {
'0'
},
KeyCode::Grave => if shift {
'~'
} else {
'`'
},
KeyCode::Minus => if shift {
'_'
} else {
'-'
},
KeyCode::Equals => if shift {
'+'
} else {
'='
},
KeyCode::LeftBracket => if shift {
'{'
} else {
'['
},
KeyCode::RightBracket => if shift {
'}'
} else {
']'
},
KeyCode::Backslash => if shift {
'|'
} else {
'\\'
},
KeyCode::Semicolon => if shift {
':'
} else {
';'
},
KeyCode::Apostrophe => if shift {
'"'
} else {
'\''
},
KeyCode::Comma => if shift {
'<'
} else {
','
},
KeyCode::Period => if shift {
'>'
} else {
'.'
},
KeyCode::Slash => if shift {
'?'
} else {
'/'
},
KeyCode::Space => ' ',
KeyCode::NumDivide => '/',
KeyCode::NumMultiply => '*',
@ -259,9 +571,7 @@ fn blt_keycode_to_char(kc: KeyCode, shift: bool) -> char {
KeyCode::Num8 => '8',
KeyCode::Num9 => '9',
KeyCode::Num0 => '0',
_ => {
unreachable!("Found unknown input: {:?}", kc)
}
_ => unreachable!("Found unknown input: {:?}", kc),
}
}
@ -297,3 +607,12 @@ fn blt_keycode_to_key(kc: KeyCode) -> Key {
_ => unreachable!(),
}
}
fn blt_keycode_to_mouse_button(kc: KeyCode) -> Option<MouseButton> {
Some(match kc {
KeyCode::MouseLeft => MouseButton::Left,
KeyCode::MouseRight => MouseButton::Right,
KeyCode::MouseMiddle => MouseButton::Middle,
_ => return None,
})
}

View File

@ -2,23 +2,27 @@ extern crate ncurses;
use self::super::find_closest;
use backend;
use event::{Event, Key};
use std::cell::{RefCell, Cell};
use event::{Event, Key, MouseButton, MouseEvent};
use std::cell::{Cell, RefCell};
use std::collections::HashMap;
use theme::{Color, ColorPair, Effect};
use utf8;
use vec::Vec2;
pub struct Concrete {
current_style: Cell<ColorPair>,
pairs: RefCell<HashMap<ColorPair, i16>>,
last_mouse_button: Option<MouseButton>,
event_queue: Vec<Event>,
}
impl Concrete {
/// Save a new color pair.
fn insert_color(&self, pairs: &mut HashMap<ColorPair, i16>,
pair: ColorPair)
-> i16 {
fn insert_color(
&self, pairs: &mut HashMap<ColorPair, i16>, pair: ColorPair
) -> i16 {
let n = 1 + pairs.len() as i16;
let target = if ncurses::COLOR_PAIRS() > i32::from(n) {
// We still have plenty of space for everyone.
@ -31,15 +35,16 @@ impl Concrete {
target
};
pairs.insert(pair, target);
ncurses::init_pair(target,
find_closest(&pair.front),
find_closest(&pair.back));
ncurses::init_pair(
target,
find_closest(&pair.front),
find_closest(&pair.back),
);
target
}
/// Checks the pair in the cache, or re-define a color if needed.
fn get_or_create(&self, pair: ColorPair) -> i16 {
let mut pairs = self.pairs.borrow_mut();
// Find if we have this color in stock
@ -52,33 +57,255 @@ impl Concrete {
}
fn set_colors(&self, pair: ColorPair) {
let i = self.get_or_create(pair);
self.current_style.set(pair);
let style = ncurses::COLOR_PAIR(i);
ncurses::attron(style);
}
fn parse_mouse_event(&mut self) -> Event {
let mut mevent = ncurses::MEVENT {
id: 0,
x: 0,
y: 0,
z: 0,
bstate: 0,
};
if ncurses::getmouse(&mut mevent as *mut ncurses::MEVENT)
== ncurses::OK
{
// eprintln!("{:032b}", mevent.bstate);
// Currently unused
let _shift = (mevent.bstate
& ncurses::BUTTON_SHIFT as ncurses::mmask_t)
!= 0;
let _alt =
(mevent.bstate & ncurses::BUTTON_ALT as ncurses::mmask_t) != 0;
let _ctrl = (mevent.bstate
& ncurses::BUTTON_CTRL as ncurses::mmask_t)
!= 0;
mevent.bstate &= !(ncurses::BUTTON_SHIFT | ncurses::BUTTON_ALT
| ncurses::BUTTON_CTRL)
as ncurses::mmask_t;
let make_event = |event| {
Event::Mouse {
offset: Vec2::zero(),
position: Vec2::new(mevent.x as usize, mevent.y as usize),
event: event,
}
};
if mevent.bstate
== ncurses::REPORT_MOUSE_POSITION as ncurses::mmask_t
{
// The event is either a mouse drag event,
// or a weird double-release event. :S
self.last_mouse_button
.map(MouseEvent::Hold)
.map(&make_event)
.unwrap_or_else(|| Event::Unknown(vec![]))
} else {
// Identify the button
let mut bare_event = mevent.bstate & ((1 << 25) - 1);
let mut event = None;
while bare_event != 0 {
let single_event = 1 << bare_event.trailing_zeros();
bare_event ^= single_event;
// Process single_event
get_event(single_event as i32, |e| if event.is_none() {
event = Some(e);
} else {
self.event_queue.push(make_event(e));
});
}
if let Some(event) = event {
if let Some(btn) = event.button() {
self.last_mouse_button = Some(btn);
}
make_event(event)
} else {
debug!("No event parsed?...");
Event::Unknown(vec![])
}
}
} else {
debug!("Ncurses event not recognized.");
Event::Unknown(vec![])
}
}
fn parse_ncurses_char(&mut self, ch: i32) -> Event {
match ch {
// Value sent by ncurses when nothing happens
-1 => Event::Refresh,
// Values under 256 are chars and control values
//
// Tab is '\t'
9 => Event::Key(Key::Tab),
// Treat '\n' and the numpad Enter the same
10 | ncurses::KEY_ENTER => Event::Key(Key::Enter),
// This is the escape key when pressed by itself.
// When used for control sequences, it should have been caught earlier.
27 => Event::Key(Key::Esc),
// `Backspace` sends 127, but Ctrl-H sends `Backspace`
127 | ncurses::KEY_BACKSPACE => Event::Key(Key::Backspace),
410 => Event::WindowResize,
// Values 512 and above are probably extensions
// Those keys don't seem to be documented...
520 => Event::Alt(Key::Del),
521 => Event::AltShift(Key::Del),
522 => Event::Ctrl(Key::Del),
523 => Event::CtrlShift(Key::Del),
//
// 524?
526 => Event::Alt(Key::Down),
527 => Event::AltShift(Key::Down),
528 => Event::Ctrl(Key::Down),
529 => Event::CtrlShift(Key::Down),
530 => Event::CtrlAlt(Key::Down),
531 => Event::Alt(Key::End),
532 => Event::AltShift(Key::End),
533 => Event::Ctrl(Key::End),
534 => Event::CtrlShift(Key::End),
535 => Event::CtrlAlt(Key::End),
536 => Event::Alt(Key::Home),
537 => Event::AltShift(Key::Home),
538 => Event::Ctrl(Key::Home),
539 => Event::CtrlShift(Key::Home),
540 => Event::CtrlAlt(Key::Home),
541 => Event::Alt(Key::Ins),
542 => Event::AltShift(Key::Ins),
543 => Event::Ctrl(Key::Ins),
// 544: CtrlShiftIns?
545 => Event::CtrlAlt(Key::Ins),
546 => Event::Alt(Key::Left),
547 => Event::AltShift(Key::Left),
548 => Event::Ctrl(Key::Left),
549 => Event::CtrlShift(Key::Left),
550 => Event::CtrlAlt(Key::Left),
551 => Event::Alt(Key::PageDown),
552 => Event::AltShift(Key::PageDown),
553 => Event::Ctrl(Key::PageDown),
554 => Event::CtrlShift(Key::PageDown),
555 => Event::CtrlAlt(Key::PageDown),
556 => Event::Alt(Key::PageUp),
557 => Event::AltShift(Key::PageUp),
558 => Event::Ctrl(Key::PageUp),
559 => Event::CtrlShift(Key::PageUp),
560 => Event::CtrlAlt(Key::PageUp),
561 => Event::Alt(Key::Right),
562 => Event::AltShift(Key::Right),
563 => Event::Ctrl(Key::Right),
564 => Event::CtrlShift(Key::Right),
565 => Event::CtrlAlt(Key::Right),
// 566?
567 => Event::Alt(Key::Up),
568 => Event::AltShift(Key::Up),
569 => Event::Ctrl(Key::Up),
570 => Event::CtrlShift(Key::Up),
571 => Event::CtrlAlt(Key::Up),
ncurses::KEY_MOUSE => self.parse_mouse_event(),
ncurses::KEY_B2 => Event::Key(Key::NumpadCenter),
ncurses::KEY_DC => Event::Key(Key::Del),
ncurses::KEY_IC => Event::Key(Key::Ins),
ncurses::KEY_BTAB => Event::Shift(Key::Tab),
ncurses::KEY_SLEFT => Event::Shift(Key::Left),
ncurses::KEY_SRIGHT => Event::Shift(Key::Right),
ncurses::KEY_LEFT => Event::Key(Key::Left),
ncurses::KEY_RIGHT => Event::Key(Key::Right),
ncurses::KEY_UP => Event::Key(Key::Up),
ncurses::KEY_DOWN => Event::Key(Key::Down),
ncurses::KEY_SR => Event::Shift(Key::Up),
ncurses::KEY_SF => Event::Shift(Key::Down),
ncurses::KEY_PPAGE => Event::Key(Key::PageUp),
ncurses::KEY_NPAGE => Event::Key(Key::PageDown),
ncurses::KEY_HOME => Event::Key(Key::Home),
ncurses::KEY_END => Event::Key(Key::End),
ncurses::KEY_SHOME => Event::Shift(Key::Home),
ncurses::KEY_SEND => Event::Shift(Key::End),
ncurses::KEY_SDC => Event::Shift(Key::Del),
ncurses::KEY_SNEXT => Event::Shift(Key::PageDown),
ncurses::KEY_SPREVIOUS => Event::Shift(Key::PageUp),
// All Fn keys use the same enum with associated number
f @ ncurses::KEY_F1...ncurses::KEY_F12 => {
Event::Key(Key::from_f((f - ncurses::KEY_F0) as u8))
}
f @ 277...288 => Event::Shift(Key::from_f((f - 276) as u8)),
f @ 289...300 => Event::Ctrl(Key::from_f((f - 288) as u8)),
f @ 301...312 => Event::CtrlShift(Key::from_f((f - 300) as u8)),
f @ 313...324 => Event::Alt(Key::from_f((f - 312) as u8)),
// Values 8-10 (H,I,J) are used by other commands,
// so we probably won't receive them. Meh~
c @ 1...25 => Event::CtrlChar((b'a' + (c - 1) as u8) as char),
other => {
// Split the i32 into 4 bytes
Event::Unknown(
(0..4)
.map(|i| ((other >> (8 * i)) & 0xFF) as u8)
.collect(),
)
}
}
}
}
impl backend::Backend for Concrete {
fn init() -> Self {
// Change the locale. For some reasons it's mandatory to get some UTF-8 support.
ncurses::setlocale(ncurses::LcCategory::all, "");
// The delay is the time ncurses wait after pressing ESC
// to see if it's an escape sequence.
// Default delay is way too long. 25 is imperceptible yet works fine.
ncurses::setlocale(ncurses::LcCategory::all, "");
::std::env::set_var("ESCDELAY", "25");
ncurses::initscr();
ncurses::keypad(ncurses::stdscr(), true);
// This disables mouse click detection,
// and provides 0-delay access to mouse presses.
ncurses::mouseinterval(0);
// Listen to all mouse events.
ncurses::mousemask(
(ncurses::ALL_MOUSE_EVENTS | ncurses::REPORT_MOUSE_POSITION)
as ncurses::mmask_t,
None,
);
ncurses::noecho();
ncurses::cbreak();
ncurses::start_color();
// Pick up background and text color from the terminal theme.
ncurses::use_default_colors();
// No cursor
ncurses::curs_set(ncurses::CURSOR_VISIBILITY::CURSOR_INVISIBLE);
// This asks the terminal to provide us with mouse drag events
// (Mouse move when a button is pressed).
// Replacing 1002 with 1003 would give us ANY mouse move.
println!("\x1B[?1002h");
Concrete {
current_style: Cell::new(ColorPair::from_256colors(0, 0)),
pairs: RefCell::new(HashMap::new()),
last_mouse_button: None,
event_queue: Vec::new(),
}
}
@ -94,6 +321,7 @@ impl backend::Backend for Concrete {
}
fn finish(&mut self) {
println!("\x1B[?1002l");
ncurses::endwin();
}
@ -123,9 +351,9 @@ impl backend::Backend for Concrete {
fn clear(&self, color: Color) {
let id = self.get_or_create(ColorPair {
front: color,
back: color,
});
front: color,
back: color,
});
ncurses::wbkgd(ncurses::stdscr(), ncurses::COLOR_PAIR(id));
ncurses::clear();
@ -140,16 +368,19 @@ impl backend::Backend for Concrete {
}
fn poll_event(&mut self) -> Event {
let ch: i32 = ncurses::getch();
self.event_queue.pop().unwrap_or_else(|| {
let ch: i32 = ncurses::getch();
// Is it a UTF-8 starting point?
if 32 <= ch && ch <= 255 && ch != 127 {
Event::Char(utf8::read_char(ch as u8,
|| Some(ncurses::getch() as u8))
.unwrap())
} else {
parse_ncurses_char(ch)
}
// Is it a UTF-8 starting point?
if 32 <= ch && ch <= 255 && ch != 127 {
Event::Char(
utf8::read_char(ch as u8, || Some(ncurses::getch() as u8))
.unwrap(),
)
} else {
self.parse_ncurses_char(ch)
}
})
}
fn set_refresh_rate(&mut self, fps: u32) {
@ -162,126 +393,85 @@ impl backend::Backend for Concrete {
}
/// Returns the Key enum corresponding to the given ncurses event.
fn parse_ncurses_char(ch: i32) -> Event {
match ch {
// Value sent by ncurses when nothing happens
-1 => Event::Refresh,
// Values under 256 are chars and control values
//
// Tab is '\t'
9 => Event::Key(Key::Tab),
// Treat '\n' and the numpad Enter the same
10 |
ncurses::KEY_ENTER => Event::Key(Key::Enter),
// This is the escape key when pressed by itself.
// When used for control sequences, it should have been caught earlier.
27 => Event::Key(Key::Esc),
// `Backspace` sends 127, but Ctrl-H sends `Backspace`
127 |
ncurses::KEY_BACKSPACE => Event::Key(Key::Backspace),
410 => Event::WindowResize,
// Values 512 and above are probably extensions
// Those keys don't seem to be documented...
520 => Event::Alt(Key::Del),
521 => Event::AltShift(Key::Del),
522 => Event::Ctrl(Key::Del),
523 => Event::CtrlShift(Key::Del),
//
// 524?
526 => Event::Alt(Key::Down),
527 => Event::AltShift(Key::Down),
528 => Event::Ctrl(Key::Down),
529 => Event::CtrlShift(Key::Down),
530 => Event::CtrlAlt(Key::Down),
531 => Event::Alt(Key::End),
532 => Event::AltShift(Key::End),
533 => Event::Ctrl(Key::End),
534 => Event::CtrlShift(Key::End),
535 => Event::CtrlAlt(Key::End),
536 => Event::Alt(Key::Home),
537 => Event::AltShift(Key::Home),
538 => Event::Ctrl(Key::Home),
539 => Event::CtrlShift(Key::Home),
540 => Event::CtrlAlt(Key::Home),
541 => Event::Alt(Key::Ins),
542 => Event::AltShift(Key::Ins),
543 => Event::Ctrl(Key::Ins),
// 544: CtrlShiftIns?
545 => Event::CtrlAlt(Key::Ins),
546 => Event::Alt(Key::Left),
547 => Event::AltShift(Key::Left),
548 => Event::Ctrl(Key::Left),
549 => Event::CtrlShift(Key::Left),
550 => Event::CtrlAlt(Key::Left),
551 => Event::Alt(Key::PageDown),
552 => Event::AltShift(Key::PageDown),
553 => Event::Ctrl(Key::PageDown),
554 => Event::CtrlShift(Key::PageDown),
555 => Event::CtrlAlt(Key::PageDown),
556 => Event::Alt(Key::PageUp),
557 => Event::AltShift(Key::PageUp),
558 => Event::Ctrl(Key::PageUp),
559 => Event::CtrlShift(Key::PageUp),
560 => Event::CtrlAlt(Key::PageUp),
561 => Event::Alt(Key::Right),
562 => Event::AltShift(Key::Right),
563 => Event::Ctrl(Key::Right),
564 => Event::CtrlShift(Key::Right),
565 => Event::CtrlAlt(Key::Right),
// 566?
567 => Event::Alt(Key::Up),
568 => Event::AltShift(Key::Up),
569 => Event::Ctrl(Key::Up),
570 => Event::CtrlShift(Key::Up),
571 => Event::CtrlAlt(Key::Up),
ncurses::KEY_B2 => Event::Key(Key::NumpadCenter),
ncurses::KEY_DC => Event::Key(Key::Del),
ncurses::KEY_IC => Event::Key(Key::Ins),
ncurses::KEY_BTAB => Event::Shift(Key::Tab),
ncurses::KEY_SLEFT => Event::Shift(Key::Left),
ncurses::KEY_SRIGHT => Event::Shift(Key::Right),
ncurses::KEY_LEFT => Event::Key(Key::Left),
ncurses::KEY_RIGHT => Event::Key(Key::Right),
ncurses::KEY_UP => Event::Key(Key::Up),
ncurses::KEY_DOWN => Event::Key(Key::Down),
ncurses::KEY_SR => Event::Shift(Key::Up),
ncurses::KEY_SF => Event::Shift(Key::Down),
ncurses::KEY_PPAGE => Event::Key(Key::PageUp),
ncurses::KEY_NPAGE => Event::Key(Key::PageDown),
ncurses::KEY_HOME => Event::Key(Key::Home),
ncurses::KEY_END => Event::Key(Key::End),
ncurses::KEY_SHOME => Event::Shift(Key::Home),
ncurses::KEY_SEND => Event::Shift(Key::End),
ncurses::KEY_SDC => Event::Shift(Key::Del),
ncurses::KEY_SNEXT => Event::Shift(Key::PageDown),
ncurses::KEY_SPREVIOUS => Event::Shift(Key::PageUp),
// All Fn keys use the same enum with associated number
f @ ncurses::KEY_F1...ncurses::KEY_F12 => {
Event::Key(Key::from_f((f - ncurses::KEY_F0) as u8))
}
f @ 277...288 => Event::Shift(Key::from_f((f - 276) as u8)),
f @ 289...300 => Event::Ctrl(Key::from_f((f - 288) as u8)),
f @ 301...312 => Event::CtrlShift(Key::from_f((f - 300) as u8)),
f @ 313...324 => Event::Alt(Key::from_f((f - 312) as u8)),
// Values 8-10 (H,I,J) are used by other commands,
// so we probably won't receive them. Meh~
c @ 1...25 => Event::CtrlChar((b'a' + (c - 1) as u8) as char),
other => {
// Split the i32 into 4 bytes
Event::Unknown((0..4)
.map(|i| ((other >> (8 * i)) & 0xFF) as u8)
.collect())
}
fn get_button(bare_event: i32) -> MouseButton {
match bare_event {
ncurses::BUTTON1_RELEASED |
ncurses::BUTTON1_PRESSED |
ncurses::BUTTON1_CLICKED |
ncurses::BUTTON1_DOUBLE_CLICKED |
ncurses::BUTTON1_TRIPLE_CLICKED => MouseButton::Left,
ncurses::BUTTON2_RELEASED |
ncurses::BUTTON2_PRESSED |
ncurses::BUTTON2_CLICKED |
ncurses::BUTTON2_DOUBLE_CLICKED |
ncurses::BUTTON2_TRIPLE_CLICKED => MouseButton::Middle,
ncurses::BUTTON3_RELEASED |
ncurses::BUTTON3_PRESSED |
ncurses::BUTTON3_CLICKED |
ncurses::BUTTON3_DOUBLE_CLICKED |
ncurses::BUTTON3_TRIPLE_CLICKED => MouseButton::Right,
ncurses::BUTTON4_RELEASED |
ncurses::BUTTON4_PRESSED |
ncurses::BUTTON4_CLICKED |
ncurses::BUTTON4_DOUBLE_CLICKED |
ncurses::BUTTON4_TRIPLE_CLICKED => MouseButton::Button4,
ncurses::BUTTON5_RELEASED |
ncurses::BUTTON5_PRESSED |
ncurses::BUTTON5_CLICKED |
ncurses::BUTTON5_DOUBLE_CLICKED |
ncurses::BUTTON5_TRIPLE_CLICKED => MouseButton::Button5,
_ => MouseButton::Other,
}
}
/// Parse the given code into one or more event.
///
/// If the given event code should expend into multiple events
/// (for instance click expends into PRESS + RELEASE),
/// the returned Vec will include those queued events.
///
/// The main event is returned separately to avoid allocation in most cases.
fn get_event<F>(bare_event: i32, mut f: F)
where
F: FnMut(MouseEvent),
{
let button = get_button(bare_event);
match bare_event {
ncurses::BUTTON4_PRESSED => f(MouseEvent::WheelUp),
ncurses::BUTTON5_PRESSED => f(MouseEvent::WheelDown),
ncurses::BUTTON1_RELEASED |
ncurses::BUTTON2_RELEASED |
ncurses::BUTTON3_RELEASED |
ncurses::BUTTON4_RELEASED |
ncurses::BUTTON5_RELEASED => f(MouseEvent::Release(button)),
ncurses::BUTTON1_PRESSED |
ncurses::BUTTON2_PRESSED |
ncurses::BUTTON3_PRESSED => f(MouseEvent::Press(button)),
ncurses::BUTTON1_CLICKED |
ncurses::BUTTON2_CLICKED |
ncurses::BUTTON3_CLICKED |
ncurses::BUTTON4_CLICKED |
ncurses::BUTTON5_CLICKED => {
f(MouseEvent::Press(button));
f(MouseEvent::Release(button));
}
// Well, we disabled click detection
ncurses::BUTTON1_DOUBLE_CLICKED |
ncurses::BUTTON2_DOUBLE_CLICKED |
ncurses::BUTTON3_DOUBLE_CLICKED |
ncurses::BUTTON4_DOUBLE_CLICKED |
ncurses::BUTTON5_DOUBLE_CLICKED => for _ in 0..2 {
f(MouseEvent::Press(button));
f(MouseEvent::Release(button));
},
ncurses::BUTTON1_TRIPLE_CLICKED |
ncurses::BUTTON2_TRIPLE_CLICKED |
ncurses::BUTTON3_TRIPLE_CLICKED |
ncurses::BUTTON4_TRIPLE_CLICKED |
ncurses::BUTTON5_TRIPLE_CLICKED => for _ in 0..3 {
f(MouseEvent::Press(button));
f(MouseEvent::Release(button));
},
_ => debug!("Unknown event: {:032b}", bare_event),
}
}

View File

@ -3,7 +3,7 @@ extern crate pancurses;
use self::super::find_closest;
use backend;
use event::{Event, Key};
use std::cell::{RefCell, Cell};
use std::cell::{Cell, RefCell};
use std::collections::HashMap;
use theme::{Color, ColorPair, Effect};
use utf8;
@ -16,10 +16,9 @@ pub struct Concrete {
impl Concrete {
/// Save a new color pair.
fn insert_color(&self, pairs: &mut HashMap<ColorPair, i32>,
pair: ColorPair)
-> i32 {
fn insert_color(
&self, pairs: &mut HashMap<ColorPair, i32>, pair: ColorPair
) -> i32 {
let n = 1 + pairs.len() as i32;
// TODO: when COLORS_PAIRS is available...
@ -34,15 +33,16 @@ impl Concrete {
target
};
pairs.insert(pair, target);
pancurses::init_pair(target as i16,
find_closest(&pair.front),
find_closest(&pair.back));
pancurses::init_pair(
target as i16,
find_closest(&pair.front),
find_closest(&pair.back),
);
target
}
/// Checks the pair in the cache, or re-define a color if needed.
fn get_or_create(&self, pair: ColorPair) -> i32 {
let mut pairs = self.pairs.borrow_mut();
// Find if we have this color in stock
@ -55,7 +55,6 @@ impl Concrete {
}
fn set_colors(&self, pair: ColorPair) {
let i = self.get_or_create(pair);
self.current_style.set(pair);
@ -121,9 +120,9 @@ impl backend::Backend for Concrete {
fn clear(&self, color: Color) {
let id = self.get_or_create(ColorPair {
front: color,
back: color,
});
front: color,
back: color,
});
self.window.bkgd(pancurses::ColorPair(id as u8));
self.window.clear();
}
@ -150,36 +149,33 @@ impl backend::Backend for Concrete {
}
pancurses::Input::Character('\u{9}') => Event::Key(Key::Tab),
pancurses::Input::Character('\u{1b}') => Event::Key(Key::Esc),
pancurses::Input::Character(c) if 32 <= (c as u32) &&
(c as u32) <= 255 => {
Event::Char(utf8::read_char(c as u8, || {
self.window
.getch()
.and_then(|i| match i {
pancurses::Input::Character(c) => {
Some(c as u8)
}
_ => None,
})
})
.unwrap())
pancurses::Input::Character(c)
if 32 <= (c as u32) && (c as u32) <= 255 =>
{
Event::Char(
utf8::read_char(c as u8, || {
self.window.getch().and_then(|i| match i {
pancurses::Input::Character(c) => {
Some(c as u8)
}
_ => None,
})
}).unwrap(),
)
}
pancurses::Input::Character(c) => {
let mut bytes = [0u8; 4];
Event::Unknown(c.encode_utf8(&mut bytes)
.as_bytes()
.to_vec())
Event::Unknown(
c.encode_utf8(&mut bytes).as_bytes().to_vec(),
)
}
// TODO: Some key combos are not recognized by pancurses,
// but are sent as Unknown. We could still parse them here.
pancurses::Input::Unknown(other) => {
Event::Unknown((0..4)
.map(|i| {
((other >> (8 * i)) & 0xFF) as
u8
})
.collect())
}
pancurses::Input::Unknown(other) => Event::Unknown(
(0..4)
.map(|i| ((other >> (8 * i)) & 0xFF) as u8)
.collect(),
),
// TODO: I honestly have no fucking idea what KeyCodeYes is
pancurses::Input::KeyCodeYes => Event::Refresh,
pancurses::Input::KeyBreak => Event::Key(Key::PauseBreak),

View File

@ -5,25 +5,28 @@ extern crate chan_signal;
use self::termion::color as tcolor;
use self::termion::event::Event as TEvent;
use self::termion::event::Key as TKey;
use self::termion::input::TermRead;
use self::termion::raw::IntoRawMode;
use self::termion::event::MouseButton as TMouseButton;
use self::termion::event::MouseEvent as TMouseEvent;
use self::termion::input::{MouseTerminal, TermRead};
use self::termion::raw::{IntoRawMode, RawTerminal};
use self::termion::screen::AlternateScreen;
use self::termion::style as tstyle;
use backend;
use chan;
use event::{Event, Key};
use event::{Event, Key, MouseButton, MouseEvent};
use std::cell::Cell;
use std::io::Write;
use std::io::{Stdout, Write};
use std::thread;
use theme;
use vec::Vec2;
pub struct Concrete {
terminal: AlternateScreen<termion::raw::RawTerminal<::std::io::Stdout>>,
terminal: AlternateScreen<MouseTerminal<RawTerminal<Stdout>>>,
current_style: Cell<theme::ColorPair>,
input: chan::Receiver<Event>,
input: chan::Receiver<TEvent>,
resize: chan::Receiver<chan_signal::Signal>,
timeout: Option<u32>,
last_button: Option<MouseButton>,
}
trait Effectable {
@ -52,6 +55,79 @@ impl Concrete {
with_color(&colors.front, |c| print!("{}", tcolor::Fg(c)));
with_color(&colors.back, |c| print!("{}", tcolor::Bg(c)));
}
fn map_key(&mut self, event: TEvent) -> Event {
match event {
TEvent::Unsupported(bytes) => Event::Unknown(bytes),
TEvent::Key(TKey::Esc) => Event::Key(Key::Esc),
TEvent::Key(TKey::Backspace) => Event::Key(Key::Backspace),
TEvent::Key(TKey::Left) => Event::Key(Key::Left),
TEvent::Key(TKey::Right) => Event::Key(Key::Right),
TEvent::Key(TKey::Up) => Event::Key(Key::Up),
TEvent::Key(TKey::Down) => Event::Key(Key::Down),
TEvent::Key(TKey::Home) => Event::Key(Key::Home),
TEvent::Key(TKey::End) => Event::Key(Key::End),
TEvent::Key(TKey::PageUp) => Event::Key(Key::PageUp),
TEvent::Key(TKey::PageDown) => Event::Key(Key::PageDown),
TEvent::Key(TKey::Delete) => Event::Key(Key::Del),
TEvent::Key(TKey::Insert) => Event::Key(Key::Ins),
TEvent::Key(TKey::F(i)) if i < 12 => Event::Key(Key::from_f(i)),
TEvent::Key(TKey::F(j)) => Event::Unknown(vec![j]),
TEvent::Key(TKey::Char('\n')) => Event::Key(Key::Enter),
TEvent::Key(TKey::Char('\t')) => Event::Key(Key::Tab),
TEvent::Key(TKey::Char(c)) => Event::Char(c),
TEvent::Key(TKey::Ctrl('c')) => Event::Exit,
TEvent::Key(TKey::Ctrl(c)) => Event::CtrlChar(c),
TEvent::Key(TKey::Alt(c)) => Event::AltChar(c),
TEvent::Mouse(TMouseEvent::Press(btn, x, y)) => {
let position = (x - 1, y - 1).into();
let event = match btn {
TMouseButton::Left => MouseEvent::Press(MouseButton::Left),
TMouseButton::Middle => {
MouseEvent::Press(MouseButton::Middle)
}
TMouseButton::Right => {
MouseEvent::Press(MouseButton::Right)
}
TMouseButton::WheelUp => MouseEvent::WheelUp,
TMouseButton::WheelDown => MouseEvent::WheelDown,
};
if let MouseEvent::Press(btn) = event {
self.last_button = Some(btn);
}
Event::Mouse {
event,
position,
offset: Vec2::zero(),
}
}
TEvent::Mouse(TMouseEvent::Release(x, y))
if self.last_button.is_some() =>
{
let event = MouseEvent::Release(self.last_button.unwrap());
let position = (x - 1, y - 1).into();
Event::Mouse {
event,
position,
offset: Vec2::zero(),
}
}
TEvent::Mouse(TMouseEvent::Hold(x, y))
if self.last_button.is_some() =>
{
let event = MouseEvent::Hold(self.last_button.unwrap());
let position = (x - 1, y - 1).into();
Event::Mouse {
event,
position,
offset: Vec2::zero(),
}
}
_ => Event::Unknown(vec![]),
}
}
}
impl backend::Backend for Concrete {
@ -60,34 +136,37 @@ impl backend::Backend for Concrete {
let resize = chan_signal::notify(&[chan_signal::Signal::WINCH]);
let terminal = AlternateScreen::from(::std::io::stdout()
.into_raw_mode()
.unwrap());
// TODO: lock stdout
let terminal = AlternateScreen::from(MouseTerminal::from(
::std::io::stdout().into_raw_mode().unwrap(),
));
let (sender, receiver) = chan::async();
thread::spawn(move || for key in ::std::io::stdin().events() {
if let Ok(key) = key {
sender.send(map_key(key))
}
});
if let Ok(key) = key {
sender.send(key)
}
});
let backend = Concrete {
Concrete {
terminal: terminal,
current_style: Cell::new(theme::ColorPair::from_256colors(0, 0)),
input: receiver,
resize: resize,
timeout: None,
};
backend
last_button: None,
}
}
fn finish(&mut self) {
print!("{}{}", termion::cursor::Show, termion::cursor::Goto(1, 1));
print!("{}[49m{}[39m{}",
27 as char,
27 as char,
termion::clear::All);
print!(
"{}[49m{}[39m{}",
27 as char,
27 as char,
termion::clear::All
);
}
fn with_color<F: FnOnce()>(&self, color: theme::ColorPair, f: F) {
@ -135,9 +214,11 @@ impl backend::Backend for Concrete {
}
fn print_at(&self, (x, y): (usize, usize), text: &str) {
print!("{}{}",
termion::cursor::Goto(1 + x as u16, 1 + y as u16),
text);
print!(
"{}{}",
termion::cursor::Goto(1 + x as u16, 1 + y as u16),
text
);
}
fn set_refresh_rate(&mut self, fps: u32) {
@ -145,91 +226,61 @@ impl backend::Backend for Concrete {
}
fn poll_event(&mut self) -> Event {
let input = &self.input;
let resize = &self.resize;
let result;
{
let input = &self.input;
let resize = &self.resize;
if let Some(timeout) = self.timeout {
let timeout = chan::after_ms(timeout);
chan_select!{
timeout.recv() => return Event::Refresh,
resize.recv() => return Event::WindowResize,
input.recv() -> input => return input.unwrap(),
}
} else {
chan_select!{
resize.recv() => return Event::WindowResize,
input.recv() -> input => return input.unwrap(),
if let Some(timeout) = self.timeout {
let timeout = chan::after_ms(timeout);
chan_select!{
timeout.recv() => return Event::Refresh,
resize.recv() => return Event::WindowResize,
input.recv() -> input => result = Some(input.unwrap()),
}
} else {
chan_select!{
resize.recv() => return Event::WindowResize,
input.recv() -> input => result = Some(input.unwrap()),
}
}
}
}
}
fn map_key(event: TEvent) -> Event {
match event {
TEvent::Unsupported(bytes) => Event::Unknown(bytes),
TEvent::Key(TKey::Esc) => Event::Key(Key::Esc),
TEvent::Key(TKey::Backspace) => Event::Key(Key::Backspace),
TEvent::Key(TKey::Left) => Event::Key(Key::Left),
TEvent::Key(TKey::Right) => Event::Key(Key::Right),
TEvent::Key(TKey::Up) => Event::Key(Key::Up),
TEvent::Key(TKey::Down) => Event::Key(Key::Down),
TEvent::Key(TKey::Home) => Event::Key(Key::Home),
TEvent::Key(TKey::End) => Event::Key(Key::End),
TEvent::Key(TKey::PageUp) => Event::Key(Key::PageUp),
TEvent::Key(TKey::PageDown) => Event::Key(Key::PageDown),
TEvent::Key(TKey::Delete) => Event::Key(Key::Del),
TEvent::Key(TKey::Insert) => Event::Key(Key::Ins),
TEvent::Key(TKey::F(i)) if i < 12 => Event::Key(Key::from_f(i)),
TEvent::Key(TKey::F(j)) => Event::Unknown(vec![j]),
TEvent::Key(TKey::Char('\n')) => Event::Key(Key::Enter),
TEvent::Key(TKey::Char('\t')) => Event::Key(Key::Tab),
TEvent::Key(TKey::Char(c)) => Event::Char(c),
TEvent::Key(TKey::Ctrl('c')) => Event::Exit,
TEvent::Key(TKey::Ctrl(c)) => Event::CtrlChar(c),
TEvent::Key(TKey::Alt(c)) => Event::AltChar(c),
_ => Event::Unknown(vec![]),
self.map_key(result.unwrap())
}
}
fn with_color<F, R>(clr: &theme::Color, f: F) -> R
where F: FnOnce(&tcolor::Color) -> R
where
F: FnOnce(&tcolor::Color) -> R,
{
match *clr {
theme::Color::TerminalDefault => f(&tcolor::Reset),
theme::Color::Dark(theme::BaseColor::Black) => f(&tcolor::Black),
theme::Color::Dark(theme::BaseColor::Red) => f(&tcolor::Red),
theme::Color::Dark(theme::BaseColor::Green) => f(&tcolor::Green),
theme::Color::Dark(theme::BaseColor::Yellow) => f(&tcolor::Yellow),
theme::Color::Dark(theme::BaseColor::Blue) => f(&tcolor::Blue),
theme::Color::Dark(theme::BaseColor::Magenta) => f(&tcolor::Magenta),
theme::Color::Dark(theme::BaseColor::Cyan) => f(&tcolor::Cyan),
theme::Color::Dark(theme::BaseColor::White) => f(&tcolor::White),
theme::Color::TerminalDefault => f(&tcolor::Reset),
theme::Color::Dark(theme::BaseColor::Black) => f(&tcolor::Black),
theme::Color::Dark(theme::BaseColor::Red) => f(&tcolor::Red),
theme::Color::Dark(theme::BaseColor::Green) => f(&tcolor::Green),
theme::Color::Dark(theme::BaseColor::Yellow) => f(&tcolor::Yellow),
theme::Color::Dark(theme::BaseColor::Blue) => f(&tcolor::Blue),
theme::Color::Dark(theme::BaseColor::Magenta) => f(&tcolor::Magenta),
theme::Color::Dark(theme::BaseColor::Cyan) => f(&tcolor::Cyan),
theme::Color::Dark(theme::BaseColor::White) => f(&tcolor::White),
theme::Color::Light(theme::BaseColor::Black) => {
f(&tcolor::LightBlack)
}
theme::Color::Light(theme::BaseColor::Red) => f(&tcolor::LightRed),
theme::Color::Light(theme::BaseColor::Green) => {
f(&tcolor::LightGreen)
}
theme::Color::Light(theme::BaseColor::Yellow) => {
f(&tcolor::LightYellow)
}
theme::Color::Light(theme::BaseColor::Blue) => f(&tcolor::LightBlue),
theme::Color::Light(theme::BaseColor::Magenta) => {
f(&tcolor::LightMagenta)
}
theme::Color::Light(theme::BaseColor::Cyan) => f(&tcolor::LightCyan),
theme::Color::Light(theme::BaseColor::White) => {
f(&tcolor::LightWhite)
}
theme::Color::Light(theme::BaseColor::Black) => f(&tcolor::LightBlack),
theme::Color::Light(theme::BaseColor::Red) => f(&tcolor::LightRed),
theme::Color::Light(theme::BaseColor::Green) => f(&tcolor::LightGreen),
theme::Color::Light(theme::BaseColor::Yellow) => {
f(&tcolor::LightYellow)
}
theme::Color::Light(theme::BaseColor::Blue) => f(&tcolor::LightBlue),
theme::Color::Light(theme::BaseColor::Magenta) => {
f(&tcolor::LightMagenta)
}
theme::Color::Light(theme::BaseColor::Cyan) => f(&tcolor::LightCyan),
theme::Color::Light(theme::BaseColor::White) => f(&tcolor::LightWhite),
theme::Color::Rgb(r, g, b) => f(&tcolor::Rgb(r, g, b)),
theme::Color::RgbLowRes(r, g, b) => {
f(&tcolor::AnsiValue::rgb(r, g, b))
}
}
theme::Color::Rgb(r, g, b) => f(&tcolor::Rgb(r, g, b)),
theme::Color::RgbLowRes(r, g, b) => {
f(&tcolor::AnsiValue::rgb(r, g, b))
}
}
}

542
src/cursive.rs Normal file
View File

@ -0,0 +1,542 @@
use backend;
use backend::Backend;
use direction;
use event::{Callback, Event, EventResult};
use printer::Printer;
use std::any::Any;
use std::collections::HashMap;
use std::path::Path;
use std::sync::mpsc;
use theme;
use vec::Vec2;
use view::{self, Finder, View};
use views;
/// Identifies a screen in the cursive root.
pub type ScreenId = usize;
/// Central part of the cursive library.
///
/// It initializes ncurses on creation and cleans up on drop.
/// To use it, you should populate it with views, layouts and callbacks,
/// then start the event loop with run().
///
/// It uses a list of screen, with one screen active at a time.
pub struct Cursive {
theme: theme::Theme,
screens: Vec<views::StackView>,
global_callbacks: HashMap<Event, Callback>,
menubar: views::Menubar,
// Last layer sizes of the stack view.
// If it changed, clear the screen.
last_sizes: Vec<Vec2>,
active_screen: ScreenId,
running: bool,
backend: backend::Concrete,
cb_source: mpsc::Receiver<Box<Fn(&mut Cursive) + Send>>,
cb_sink: mpsc::Sender<Box<Fn(&mut Cursive) + Send>>,
}
new_default!(Cursive);
impl Cursive {
/// Creates a new Cursive root, and initialize the back-end.
pub fn new() -> Self {
let backend = backend::Concrete::init();
let theme = theme::load_default();
// theme.activate(&mut backend);
// let theme = theme::load_theme("assets/style.toml").unwrap();
let (tx, rx) = mpsc::channel();
let mut res = Cursive {
theme: theme,
screens: Vec::new(),
last_sizes: Vec::new(),
global_callbacks: HashMap::new(),
menubar: views::Menubar::new(),
active_screen: 0,
running: true,
cb_source: rx,
cb_sink: tx,
backend: backend,
};
res.screens.push(views::StackView::new());
res
}
/// Returns a sink for asynchronous callbacks.
///
/// Returns the sender part of a channel, that allows to send
/// callbacks to `self` from other threads.
///
/// Callbacks will be executed in the order
/// of arrival on the next event cycle.
///
/// Note that you currently need to call [`set_fps`] to force cursive to
/// regularly check for messages.
///
/// [`set_fps`]: #method.set_fps
pub fn cb_sink(&self) -> &mpsc::Sender<Box<Fn(&mut Cursive) + Send>> {
&self.cb_sink
}
/// Selects the menubar.
pub fn select_menubar(&mut self) {
self.menubar.take_focus(direction::Direction::none());
}
/// Sets the menubar autohide feature.
///
/// * When enabled (default), the menu is only visible when selected.
/// * When disabled, the menu is always visible and reserves the top row.
pub fn set_autohide_menu(&mut self, autohide: bool) {
self.menubar.autohide = autohide;
}
/// Access the menu tree used by the menubar.
///
/// This allows to add menu items to the menubar.
///
/// # Examples
///
/// ```no_run
/// # extern crate cursive;
/// #
/// # use cursive::{Cursive, event};
/// # use cursive::views::{Dialog};
/// # use cursive::traits::*;
/// # use cursive::menu::*;
/// #
/// # fn main() {
/// let mut siv = Cursive::new();
///
/// siv.menubar()
/// .add_subtree("File",
/// MenuTree::new()
/// .leaf("New", |s| s.add_layer(Dialog::info("New file!")))
/// .subtree("Recent", MenuTree::new().with(|tree| {
/// for i in 1..100 {
/// tree.add_leaf(format!("Item {}", i), |_| ())
/// }
/// }))
/// .delimiter()
/// .with(|tree| {
/// for i in 1..10 {
/// tree.add_leaf(format!("Option {}", i), |_| ());
/// }
/// })
/// .delimiter()
/// .leaf("Quit", |s| s.quit()))
/// .add_subtree("Help",
/// MenuTree::new()
/// .subtree("Help",
/// MenuTree::new()
/// .leaf("General", |s| {
/// s.add_layer(Dialog::info("Help message!"))
/// })
/// .leaf("Online", |s| {
/// s.add_layer(Dialog::info("Online help?"))
/// }))
/// .leaf("About",
/// |s| s.add_layer(Dialog::info("Cursive v0.0.0"))));
///
/// siv.add_global_callback(event::Key::Esc, |s| s.select_menubar());
/// # }
/// ```
pub fn menubar(&mut self) -> &mut views::Menubar {
&mut self.menubar
}
/// Returns the currently used theme.
pub fn current_theme(&self) -> &theme::Theme {
&self.theme
}
/// Sets the current theme.
pub fn set_theme(&mut self, theme: theme::Theme) {
self.theme = theme;
self.clear();
}
/// Clears the screen.
///
/// Users rarely have to call this directly.
pub fn clear(&self) {
self.backend.clear(self.theme.colors.background);
}
/// Loads a theme from the given file.
///
/// `filename` must point to a valid toml file.
pub fn load_theme_file<P: AsRef<Path>>(
&mut self, filename: P
) -> Result<(), theme::Error> {
self.set_theme(try!(theme::load_theme_file(filename)));
Ok(())
}
/// Loads a theme from the given string content.
///
/// Content must be valid toml.
pub fn load_theme(&mut self, content: &str) -> Result<(), theme::Error> {
self.set_theme(try!(theme::load_theme(content)));
Ok(())
}
/// Sets the refresh rate, in frames per second.
///
/// Regularly redraws everything, even when no input is given.
///
/// You currently need this to regularly check
/// for events sent using [`cb_sink`].
///
/// Between 0 and 1000. Call with `fps = 0` to disable (default value).
///
/// [`cb_sink`]: #method.cb_sink
pub fn set_fps(&mut self, fps: u32) {
self.backend.set_refresh_rate(fps)
}
/// Returns a reference to the currently active screen.
pub fn screen(&self) -> &views::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 views::StackView {
let id = self.active_screen;
&mut self.screens[id]
}
/// Adds a new screen, and returns its ID.
pub fn add_screen(&mut self) -> ScreenId {
let res = self.screens.len();
self.screens.push(views::StackView::new());
res
}
/// Convenient method to create a new screen, and set it as active.
pub fn add_active_screen(&mut self) -> ScreenId {
let res = self.add_screen();
self.set_screen(res);
res
}
/// Sets the active screen. Panics if no such screen exist.
pub fn set_screen(&mut self, screen_id: ScreenId) {
if screen_id >= self.screens.len() {
panic!(
"Tried to set an invalid screen ID: {}, but only {} \
screens present.",
screen_id,
self.screens.len()
);
}
self.active_screen = screen_id;
}
/// Tries to find the view pointed to by the given selector.
///
/// Runs a closure on the view once it's found, and return the
/// result.
///
/// If the view is not found, or if it is not of the asked type,
/// returns None.
///
/// # Examples
///
/// ```no_run
/// # extern crate cursive;
/// # use cursive::{Cursive, views, view};
/// # use cursive::traits::*;
/// # fn main() {
/// let mut siv = Cursive::new();
///
/// siv.add_layer(views::TextView::new("Text #1")
/// .with_id("text"));
///
/// siv.add_global_callback('p', |s| {
/// s.call_on(&view::Selector::Id("text"), |view: &mut views::TextView| {
/// view.set_content("Text #2");
/// });
/// });
/// # }
/// ```
pub fn call_on<V, F, R>(
&mut self, sel: &view::Selector, callback: F
) -> Option<R>
where
V: View + Any,
F: FnOnce(&mut V) -> R,
{
self.screen_mut().call_on(sel, callback)
}
/// Tries to find the view identified by the given id.
///
/// Convenient method to use `call_on` with a `view::Selector::Id`.
///
/// # Examples
///
/// ```no_run
/// # extern crate cursive;
/// # use cursive::{Cursive, views};
/// # use cursive::traits::*;
/// # fn main() {
/// let mut siv = Cursive::new();
///
/// siv.add_layer(views::TextView::new("Text #1")
/// .with_id("text"));
///
/// siv.add_global_callback('p', |s| {
/// s.call_on_id("text", |view: &mut views::TextView| {
/// view.set_content("Text #2");
/// });
/// });
/// # }
/// ```
pub fn call_on_id<V, F, R>(&mut self, id: &str, callback: F) -> Option<R>
where
V: View + Any,
F: FnOnce(&mut V) -> R,
{
self.call_on(&view::Selector::Id(id), callback)
}
/// Convenient method to find a view wrapped in [`IdView`].
///
/// This looks for a `IdView<V>` with the given ID, and return
/// a mutable reference to the wrapped view.
///
/// [`IdView`]: views/struct.IdView.html
pub fn find_id<V>(&mut self, id: &str) -> Option<views::ViewRef<V>>
where
V: View + Any,
{
self.call_on_id(id, views::IdView::<V>::get_mut)
}
/// Moves the focus to the view identified by `id`.
///
/// Convenient method to call `focus` with a `view::Selector::Id`.
pub fn focus_id(&mut self, id: &str) -> Result<(), ()> {
self.focus(&view::Selector::Id(id))
}
/// Moves the focus to the view identified by `sel`.
pub fn focus(&mut self, sel: &view::Selector) -> Result<(), ()> {
self.screen_mut().focus_view(sel)
}
/// Adds a global callback.
///
/// Will be triggered on the given key press when no view catches it.
///
/// # Examples
///
/// ```no_run
/// # extern crate cursive;
/// # use cursive::*;
/// # fn main() {
/// let mut siv = Cursive::new();
///
/// siv.add_global_callback('q', |s| s.quit());
/// # }
/// ```
pub fn add_global_callback<F, E: Into<Event>>(&mut self, event: E, cb: F)
where
F: Fn(&mut Cursive) + 'static,
{
self.global_callbacks
.insert(event.into(), Callback::from_fn(cb));
}
/// Add a layer to the current screen.
///
/// # Examples
///
/// ```no_run
/// # extern crate cursive;
/// # use cursive::*;
/// # fn main() {
/// let mut siv = Cursive::new();
///
/// siv.add_layer(views::TextView::new("Hello world!"));
/// # }
/// ```
pub fn add_layer<T: 'static + View>(&mut self, view: T) {
self.screen_mut().add_layer(view);
}
/// Adds a new full-screen layer to the current screen.
///
/// Fullscreen layers have no shadow.
pub fn add_fullscreen_layer<T>(&mut self, view: T)
where
T: 'static + View,
{
self.screen_mut().add_fullscreen_layer(view);
}
/// Convenient method to remove a layer from the current screen.
pub fn pop_layer(&mut self) {
self.screen_mut().pop_layer();
self.clear();
}
// Handles a key event when it was ignored by the current view
fn on_event(&mut self, event: Event) {
let cb = match self.global_callbacks.get(&event) {
None => return,
Some(cb) => cb.clone(),
};
// Not from a view, so no viewpath here
cb(self);
}
/// Returns the size of the screen, in characters.
pub fn screen_size(&self) -> Vec2 {
let (x, y) = self.backend.screen_size();
Vec2 {
x: x as usize,
y: y as usize,
}
}
fn layout(&mut self) {
let size = self.screen_size();
self.screen_mut().layout(size);
}
fn draw(&mut self) {
let sizes = self.screen().layer_sizes();
if self.last_sizes != sizes {
self.clear();
self.last_sizes = sizes;
}
let printer =
Printer::new(self.screen_size(), &self.theme, &self.backend);
// Draw the currently active screen
// If the menubar is active, nothing else can be.
let offset = if self.menubar.autohide { 0 } else { 1 };
// Draw the menubar?
if self.menubar.visible() {
let printer = printer.sub_printer(
Vec2::zero(),
printer.size,
self.menubar.receive_events(),
);
self.menubar.draw(&printer);
}
let selected = self.menubar.receive_events();
let printer =
printer.sub_printer(Vec2::new(0, offset), printer.size, !selected);
let id = self.active_screen;
self.screens[id].draw(&printer);
}
/// Returns `true` until [`quit(&mut self)`] is called.
///
/// [`quit(&mut self)`]: #method.quit
pub fn is_running(&self) -> bool {
self.running
}
/// Runs the event loop.
///
/// It will wait for user input (key presses)
/// and trigger callbacks accordingly.
///
/// Calls [`step(&mut self)`] until [`quit(&mut self)`] is called.
///
/// After this function returns, you can call
/// it again and it will start a new loop.
///
/// [`step(&mut self)`]: #method.step
/// [`quit(&mut self)`]: #method.quit
pub fn run(&mut self) {
self.running = true;
// And the big event loop begins!
while self.running {
self.step();
}
}
/// Performs a single step from the event loop.
///
/// Useful if you need tighter control on the event loop.
/// Otherwise, [`run(&mut self)`] might be more convenient.
///
/// [`run(&mut self)`]: #method.run
pub fn step(&mut self) {
if let Ok(cb) = self.cb_source.try_recv() {
cb(self);
}
// Do we need to redraw everytime?
// Probably, actually.
// TODO: Do we need to re-layout everytime?
self.layout();
// TODO: Do we need to redraw every view every time?
// (Is this getting repetitive? :p)
self.draw();
self.backend.refresh();
// Wait for next event.
// (If set_fps was called, this returns -1 now and then)
let event = self.backend.poll_event();
if event == Event::Exit {
self.quit();
}
if event == Event::WindowResize {
self.clear();
}
// Event dispatch order:
// * Focused element:
// * Menubar (if active)
// * Current screen (top layer)
// * Global callbacks
if self.menubar.receive_events() {
self.menubar.on_event(event).process(self);
} else {
match self.screen_mut().on_event(event.clone()) {
// If the event was ignored,
// it is our turn to play with it.
EventResult::Ignored => self.on_event(event),
EventResult::Consumed(None) => (),
EventResult::Consumed(Some(cb)) => cb(self),
}
}
}
/// Stops the event loop.
pub fn quit(&mut self) {
self.running = false;
}
}
impl Drop for Cursive {
fn drop(&mut self) {
self.backend.finish();
}
}

View File

@ -19,7 +19,7 @@ use XY;
use vec::Vec2;
/// Describes a vertical or horizontal orientation for a view.
#[derive(Clone,Copy,Debug,PartialEq)]
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Orientation {
/// Horizontal orientation
Horizontal,

View File

@ -1,15 +1,15 @@
use num::Num;
// Integer division that rounds up.
// pub fn div_up_usize(p: usize, q: usize) -> usize {
// div_up(p as u32, q as u32) as usize
// }
//
// Integer division that rounds up.
// pub fn div_up(p: u32, q: u32) -> u32 {
// if p % q == 0 {
// p / q
// } else {
// 1 + p / q
// }
// }
//
/// Integer division that rounds up.
pub fn div_up<T>(p: T, q: T) -> T
where
T: Num + Clone,
{
let d = p.clone() / q.clone();
if p % q == T::zero() {
d
} else {
T::one() + d
}
}

View File

@ -17,6 +17,7 @@
use Cursive;
use std::ops::Deref;
use std::rc::Rc;
use vec::Vec2;
/// Callback is a function that can be triggered by an event.
/// It has a mutable access to the cursive root.
@ -196,6 +197,66 @@ impl Key {
}
}
/// One of the buttons present on the mouse
#[derive(PartialEq, Eq, Clone, Copy, Hash, Debug)]
pub enum MouseButton {
/// The left button, used for main actions.
Left,
/// Middle button, probably the wheel. Often pastes text in X11 on linux.
Middle,
/// The right button, for special actions.
Right,
/// Fourth button if the mouse supports it.
Button4,
/// Fifth button if the mouse supports it.
Button5,
// TODO: handle more buttons?
#[doc(hidden)] Other,
}
/// Represents a possible event sent by the mouse.
#[derive(PartialEq, Eq, Clone, Copy, Hash, Debug)]
pub enum MouseEvent {
/// A button was pressed.
Press(MouseButton),
/// A button was released.
Release(MouseButton),
/// A button is being held.
Hold(MouseButton),
/// The wheel was moved up.
WheelUp,
/// The wheel was moved down.
WheelDown,
}
impl MouseEvent {
/// Returns the button used by this event, if any.
///
/// Returns `None` if `self` is `WheelUp` or `WheelDown`.
pub fn button(&self) -> Option<MouseButton> {
match *self {
MouseEvent::Press(btn) |
MouseEvent::Release(btn) |
MouseEvent::Hold(btn) => Some(btn),
_ => None,
}
}
/// Returns `true` if `self` is an event that can grab focus.
///
/// This includes `Press`, `WheelUp` and `WheelDown`.
pub fn grabs_focus(self) -> bool {
match self {
MouseEvent::Press(_) |
MouseEvent::WheelUp |
MouseEvent::WheelDown => true,
_ => false,
}
}
}
/// Represents an event as seen by the application.
#[derive(PartialEq, Eq, Clone, Hash, Debug)]
pub enum Event {
@ -205,6 +266,7 @@ pub enum Event {
/// Event fired regularly when a auto-refresh is set.
Refresh,
// TODO: have Char(modifier, char) and Key(modifier, key) enums?
/// A character was entered (includes numbers, punctuation, ...).
Char(char),
/// A character was entered with the Ctrl key pressed.
@ -227,14 +289,64 @@ pub enum Event {
/// A non-character key was pressed with the Ctrl and Alt keys pressed.
CtrlAlt(Key),
/// A mouse event was sent.
Mouse {
/// Position of the top-left corner of the view receiving this event.
offset: Vec2,
/// Position of the mouse when this event was fired.
position: Vec2,
/// The mouse event itself.
event: MouseEvent,
},
// TODO: use a backend-dependent type for the unknown values?
/// An unknown event was received.
Unknown(Vec<u8>),
// Having a doc-hidden event prevents people from having exhaustive matches,
// allowing us to add events in the future.
#[doc(hidden)]
/// The application is about to exit.
Exit,
}
impl Event {
/// Returns the position of the mouse, if `self` is a mouse event.
pub fn mouse_position(&self) -> Option<Vec2> {
if let Event::Mouse { position, .. } = *self {
Some(position)
} else {
None
}
}
/// Update `self` with the given offset.
///
/// If `self` is a mouse event, adds `top_left` to its offset.
/// Otherwise, do nothing.
pub fn relativize<V>(&mut self, top_left: V)
where
V: Into<Vec2>,
{
if let Event::Mouse { ref mut offset, .. } = *self {
*offset = *offset + top_left;
}
}
/// Returns a cloned, relativized event.
///
/// If `self` is a mouse event, adds `top_left` to its offset.
/// Otherwise, returns a simple clone.
pub fn relativized<V>(&self, top_left: V) -> Self
where
V: Into<Vec2>,
{
let mut result = self.clone();
result.relativize(top_left);
result
}
}
impl From<char> for Event {
fn from(c: char) -> Event {
Event::Char(c)

View File

@ -97,6 +97,7 @@ pub mod direction;
pub mod utils;
// This probably doesn't need to be public?
mod cursive;
mod printer;
mod xy;
mod with;
@ -108,540 +109,7 @@ mod utf8;
pub mod backend;
use backend::Backend;
use event::{Callback, Event, EventResult};
pub use cursive::{Cursive, ScreenId};
pub use printer::Printer;
use std::any::Any;
use std::collections::HashMap;
use std::path::Path;
use std::sync::mpsc;
use vec::Vec2;
use view::Finder;
use view::View;
pub use with::With;
pub use xy::XY;
/// Identifies a screen in the cursive root.
pub type ScreenId = usize;
/// Central part of the cursive library.
///
/// It initializes ncurses on creation and cleans up on drop.
/// To use it, you should populate it with views, layouts and callbacks,
/// then start the event loop with run().
///
/// It uses a list of screen, with one screen active at a time.
pub struct Cursive {
theme: theme::Theme,
screens: Vec<views::StackView>,
global_callbacks: HashMap<Event, Callback>,
menubar: views::Menubar,
// Last layer sizes of the stack view.
// If it changed, clear the screen.
last_sizes: Vec<Vec2>,
active_screen: ScreenId,
running: bool,
backend: backend::Concrete,
cb_source: mpsc::Receiver<Box<Fn(&mut Cursive) + Send>>,
cb_sink: mpsc::Sender<Box<Fn(&mut Cursive) + Send>>,
}
new_default!(Cursive);
impl Cursive {
/// Creates a new Cursive root, and initialize the back-end.
pub fn new() -> Self {
let backend = backend::Concrete::init();
let theme = theme::load_default();
// theme.activate(&mut backend);
// let theme = theme::load_theme("assets/style.toml").unwrap();
let (tx, rx) = mpsc::channel();
let mut res = Cursive {
theme: theme,
screens: Vec::new(),
last_sizes: Vec::new(),
global_callbacks: HashMap::new(),
menubar: views::Menubar::new(),
active_screen: 0,
running: true,
cb_source: rx,
cb_sink: tx,
backend: backend,
};
res.screens.push(views::StackView::new());
res
}
/// Returns a sink for asynchronous callbacks.
///
/// Returns the sender part of a channel, that allows to send
/// callbacks to `self` from other threads.
///
/// Callbacks will be executed in the order
/// of arrival on the next event cycle.
///
/// Note that you currently need to call [`set_fps`] to force cursive to
/// regularly check for messages.
///
/// [`set_fps`]: #method.set_fps
pub fn cb_sink(&self) -> &mpsc::Sender<Box<Fn(&mut Cursive) + Send>> {
&self.cb_sink
}
/// Selects the menubar.
pub fn select_menubar(&mut self) {
self.menubar.take_focus(direction::Direction::none());
}
/// Sets the menubar autohide feature.
///
/// * When enabled (default), the menu is only visible when selected.
/// * When disabled, the menu is always visible and reserves the top row.
pub fn set_autohide_menu(&mut self, autohide: bool) {
self.menubar.autohide = autohide;
}
/// Access the menu tree used by the menubar.
///
/// This allows to add menu items to the menubar.
///
/// # Examples
///
/// ```no_run
/// # extern crate cursive;
/// #
/// # use cursive::{Cursive, event};
/// # use cursive::views::{Dialog};
/// # use cursive::traits::*;
/// # use cursive::menu::*;
/// #
/// # fn main() {
/// let mut siv = Cursive::new();
///
/// siv.menubar()
/// .add_subtree("File",
/// MenuTree::new()
/// .leaf("New", |s| s.add_layer(Dialog::info("New file!")))
/// .subtree("Recent", MenuTree::new().with(|tree| {
/// for i in 1..100 {
/// tree.add_leaf(format!("Item {}", i), |_| ())
/// }
/// }))
/// .delimiter()
/// .with(|tree| {
/// for i in 1..10 {
/// tree.add_leaf(format!("Option {}", i), |_| ());
/// }
/// })
/// .delimiter()
/// .leaf("Quit", |s| s.quit()))
/// .add_subtree("Help",
/// MenuTree::new()
/// .subtree("Help",
/// MenuTree::new()
/// .leaf("General", |s| {
/// s.add_layer(Dialog::info("Help message!"))
/// })
/// .leaf("Online", |s| {
/// s.add_layer(Dialog::info("Online help?"))
/// }))
/// .leaf("About",
/// |s| s.add_layer(Dialog::info("Cursive v0.0.0"))));
///
/// siv.add_global_callback(event::Key::Esc, |s| s.select_menubar());
/// # }
/// ```
pub fn menubar(&mut self) -> &mut views::Menubar {
&mut self.menubar
}
/// Returns the currently used theme.
pub fn current_theme(&self) -> &theme::Theme {
&self.theme
}
/// Sets the current theme.
pub fn set_theme(&mut self, theme: theme::Theme) {
self.theme = theme;
self.clear();
}
/// Clears the screen.
///
/// Users rarely have to call this directly.
pub fn clear(&self) {
self.backend.clear(self.theme.colors.background);
}
/// Loads a theme from the given file.
///
/// `filename` must point to a valid toml file.
pub fn load_theme_file<P: AsRef<Path>>(&mut self, filename: P)
-> Result<(), theme::Error> {
self.set_theme(try!(theme::load_theme_file(filename)));
Ok(())
}
/// Loads a theme from the given string content.
///
/// Content must be valid toml.
pub fn load_theme(&mut self, content: &str) -> Result<(), theme::Error> {
self.set_theme(try!(theme::load_theme(content)));
Ok(())
}
/// Sets the refresh rate, in frames per second.
///
/// Regularly redraws everything, even when no input is given.
///
/// You currently need this to regularly check
/// for events sent using [`cb_sink`].
///
/// Between 0 and 1000. Call with `fps = 0` to disable (default value).
///
/// [`cb_sink`]: #method.cb_sink
pub fn set_fps(&mut self, fps: u32) {
self.backend.set_refresh_rate(fps)
}
/// Returns a reference to the currently active screen.
pub fn screen(&self) -> &views::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 views::StackView {
let id = self.active_screen;
&mut self.screens[id]
}
/// Adds a new screen, and returns its ID.
pub fn add_screen(&mut self) -> ScreenId {
let res = self.screens.len();
self.screens.push(views::StackView::new());
res
}
/// Convenient method to create a new screen, and set it as active.
pub fn add_active_screen(&mut self) -> ScreenId {
let res = self.add_screen();
self.set_screen(res);
res
}
/// Sets the active screen. Panics if no such screen exist.
pub fn set_screen(&mut self, screen_id: ScreenId) {
if screen_id >= self.screens.len() {
panic!("Tried to set an invalid screen ID: {}, but only {} \
screens present.",
screen_id,
self.screens.len());
}
self.active_screen = screen_id;
}
/// Tries to find the view pointed to by the given selector.
///
/// Runs a closure on the view once it's found, and return the
/// result.
///
/// If the view is not found, or if it is not of the asked type,
/// returns None.
///
/// # Examples
///
/// ```no_run
/// # extern crate cursive;
/// # use cursive::{Cursive, views, view};
/// # use cursive::traits::*;
/// # fn main() {
/// let mut siv = Cursive::new();
///
/// siv.add_layer(views::TextView::new("Text #1")
/// .with_id("text"));
///
/// siv.add_global_callback('p', |s| {
/// s.call_on(&view::Selector::Id("text"), |view: &mut views::TextView| {
/// view.set_content("Text #2");
/// });
/// });
/// # }
/// ```
pub fn call_on<V, F, R>(&mut self, sel: &view::Selector, callback: F)
-> Option<R>
where V: View + Any,
F: FnOnce(&mut V) -> R
{
self.screen_mut().call_on(sel, callback)
}
/// Tries to find the view identified by the given id.
///
/// Convenient method to use `call_on` with a `view::Selector::Id`.
///
/// # Examples
///
/// ```no_run
/// # extern crate cursive;
/// # use cursive::{Cursive, views};
/// # use cursive::traits::*;
/// # fn main() {
/// let mut siv = Cursive::new();
///
/// siv.add_layer(views::TextView::new("Text #1")
/// .with_id("text"));
///
/// siv.add_global_callback('p', |s| {
/// s.call_on_id("text", |view: &mut views::TextView| {
/// view.set_content("Text #2");
/// });
/// });
/// # }
/// ```
pub fn call_on_id<V, F, R>(&mut self, id: &str, callback: F) -> Option<R>
where V: View + Any,
F: FnOnce(&mut V) -> R
{
self.call_on(&view::Selector::Id(id), callback)
}
/// Convenient method to find a view wrapped in [`IdView`].
///
/// This looks for a `IdView<V>` with the given ID, and return
/// a mutable reference to the wrapped view.
///
/// [`IdView`]: views/struct.IdView.html
pub fn find_id<V>(&mut self, id: &str) -> Option<views::ViewRef<V>>
where V: View + Any
{
self.call_on_id(id, views::IdView::<V>::get_mut)
}
/// Moves the focus to the view identified by `id`.
///
/// Convenient method to call `focus` with a `view::Selector::Id`.
pub fn focus_id(&mut self, id: &str) -> Result<(), ()> {
self.focus(&view::Selector::Id(id))
}
/// Moves the focus to the view identified by `sel`.
pub fn focus(&mut self, sel: &view::Selector) -> Result<(), ()> {
self.screen_mut().focus_view(sel)
}
/// Adds a global callback.
///
/// Will be triggered on the given key press when no view catches it.
///
/// # Examples
///
/// ```no_run
/// # extern crate cursive;
/// # use cursive::*;
/// # fn main() {
/// let mut siv = Cursive::new();
///
/// siv.add_global_callback('q', |s| s.quit());
/// # }
/// ```
pub fn add_global_callback<F, E: Into<Event>>(&mut self, event: E, cb: F)
where F: Fn(&mut Cursive) + 'static
{
self.global_callbacks.insert(event.into(), Callback::from_fn(cb));
}
/// Add a layer to the current screen.
///
/// # Examples
///
/// ```no_run
/// # extern crate cursive;
/// # use cursive::*;
/// # fn main() {
/// let mut siv = Cursive::new();
///
/// siv.add_layer(views::TextView::new("Hello world!"));
/// # }
/// ```
pub fn add_layer<T: 'static + View>(&mut self, view: T) {
self.screen_mut().add_layer(view);
}
/// Adds a new full-screen layer to the current screen.
///
/// Fullscreen layers have no shadow.
pub fn add_fullscreen_layer<T>(&mut self, view: T)
where T: 'static + View
{
self.screen_mut().add_fullscreen_layer(view);
}
/// Convenient method to remove a layer from the current screen.
pub fn pop_layer(&mut self) {
self.screen_mut().pop_layer();
self.clear();
}
// Handles a key event when it was ignored by the current view
fn on_event(&mut self, event: Event) {
let cb = match self.global_callbacks.get(&event) {
None => return,
Some(cb) => cb.clone(),
};
// Not from a view, so no viewpath here
cb(self);
}
/// Returns the size of the screen, in characters.
pub fn screen_size(&self) -> Vec2 {
let (x, y) = self.backend.screen_size();
Vec2 {
x: x as usize,
y: y as usize,
}
}
fn layout(&mut self) {
let size = self.screen_size();
self.screen_mut().layout(size);
}
fn draw(&mut self) {
let sizes = self.screen().layer_sizes();
if self.last_sizes != sizes {
self.clear();
self.last_sizes = sizes;
}
let printer = Printer::new(self.screen_size(),
&self.theme,
&self.backend);
// Draw the currently active screen
// If the menubar is active, nothing else can be.
let offset = if self.menubar.autohide { 0 } else { 1 };
// Draw the menubar?
if self.menubar.visible() {
let printer = printer.sub_printer(Vec2::zero(),
printer.size,
self.menubar.receive_events());
self.menubar.draw(&printer);
}
let selected = self.menubar.receive_events();
let printer =
printer.sub_printer(Vec2::new(0, offset), printer.size, !selected);
let id = self.active_screen;
self.screens[id].draw(&printer);
}
/// Returns `true` until [`quit(&mut self)`] is called.
///
/// [`quit(&mut self)`]: #method.quit
pub fn is_running(&self) -> bool {
self.running
}
/// Runs the event loop.
///
/// It will wait for user input (key presses)
/// and trigger callbacks accordingly.
///
/// Calls [`step(&mut self)`] until [`quit(&mut self)`] is called.
///
/// After this function returns, you can call
/// it again and it will start a new loop.
///
/// [`step(&mut self)`]: #method.step
/// [`quit(&mut self)`]: #method.quit
pub fn run(&mut self) {
self.running = true;
// And the big event loop begins!
while self.running {
self.step();
}
}
/// Performs a single step from the event loop.
///
/// Useful if you need tighter control on the event loop.
/// Otherwise, [`run(&mut self)`] might be more convenient.
///
/// [`run(&mut self)`]: #method.run
pub fn step(&mut self) {
if let Ok(cb) = self.cb_source.try_recv() {
cb(self);
}
// Do we need to redraw everytime?
// Probably, actually.
// TODO: Do we need to re-layout everytime?
self.layout();
// TODO: Do we need to redraw every view every time?
// (Is this getting repetitive? :p)
self.draw();
self.backend.refresh();
// Wait for next event.
// (If set_fps was called, this returns -1 now and then)
let event = self.backend.poll_event();
if event == Event::Exit {
self.quit();
}
if event == Event::WindowResize {
self.clear();
}
// Event dispatch order:
// * Focused element:
// * Menubar (if active)
// * Current screen (top layer)
// * Global callbacks
if self.menubar.receive_events() {
self.menubar.on_event(event).process(self);
} else {
match self.screen_mut().on_event(event.clone()) {
// If the event was ignored,
// it is our turn to play with it.
EventResult::Ignored => self.on_event(event),
EventResult::Consumed(None) => (),
EventResult::Consumed(Some(cb)) => cb(self),
}
}
}
/// Stops the event loop.
pub fn quit(&mut self) {
self.running = false;
}
}
impl Drop for Cursive {
fn drop(&mut self) {
self.backend.finish();
}
}

View File

@ -42,8 +42,9 @@ impl MenuItem {
pub fn label(&self) -> &str {
match *self {
MenuItem::Delimiter => "",
MenuItem::Leaf(ref label, _) |
MenuItem::Subtree(ref label, _) => label,
MenuItem::Leaf(ref label, _) | MenuItem::Subtree(ref label, _) => {
label
}
}
}
@ -93,8 +94,9 @@ impl MenuTree {
/// Adds a actionnable leaf to the end of this tree.
pub fn add_leaf<S, F>(&mut self, title: S, cb: F)
where S: Into<String>,
F: 'static + Fn(&mut Cursive)
where
S: Into<String>,
F: 'static + Fn(&mut Cursive),
{
let i = self.children.len();
self.insert_leaf(i, title, cb);
@ -102,25 +104,29 @@ impl MenuTree {
/// Inserts a leaf at the given position.
pub fn insert_leaf<S, F>(&mut self, i: usize, title: S, cb: F)
where S: Into<String>,
F: 'static + Fn(&mut Cursive)
where
S: Into<String>,
F: 'static + Fn(&mut Cursive),
{
let title = title.into();
self.children.insert(i, MenuItem::Leaf(title, Callback::from_fn(cb)));
self.children
.insert(i, MenuItem::Leaf(title, Callback::from_fn(cb)));
}
/// Adds a actionnable leaf to the end of this tree - chainable variant.
pub fn leaf<S, F>(self, title: S, cb: F) -> Self
where S: Into<String>,
F: 'static + Fn(&mut Cursive)
where
S: Into<String>,
F: 'static + Fn(&mut Cursive),
{
self.with(|menu| menu.add_leaf(title, cb))
}
/// Inserts a subtree at the given position.
pub fn insert_subtree<S>(&mut self, i: usize, title: S, tree: MenuTree)
where S: Into<String>
where
S: Into<String>,
{
let title = title.into();
let tree = MenuItem::Subtree(title, Rc::new(tree));
@ -129,7 +135,8 @@ impl MenuTree {
/// Adds a submenu to the end of this tree.
pub fn add_subtree<S>(&mut self, title: S, tree: MenuTree)
where S: Into<String>
where
S: Into<String>,
{
let i = self.children.len();
self.insert_subtree(i, title, tree);
@ -137,7 +144,8 @@ impl MenuTree {
/// Adds a submenu to the end of this tree - chainable variant.
pub fn subtree<S>(self, title: S, tree: MenuTree) -> Self
where S: Into<String>
where
S: Into<String>,
{
self.with(|menu| menu.add_subtree(title, tree))
}
@ -165,13 +173,13 @@ impl MenuTree {
/// Returns `None` if the given title was not found,
/// or if it wasn't a subtree.
pub fn find_subtree(&mut self, title: &str) -> Option<&mut MenuTree> {
self.find_item(title)
.and_then(|item| if let MenuItem::Subtree(_, ref mut tree) =
*item {
self.find_item(title).and_then(
|item| if let MenuItem::Subtree(_, ref mut tree) = *item {
Some(Rc::make_mut(tree))
} else {
None
})
},
)
}
/// Removes the item at the given position.

View File

@ -5,10 +5,8 @@ use backend::{self, Backend};
use std::cell::Cell;
use std::cmp::min;
use std::rc::Rc;
use theme::{BorderStyle, ColorStyle, Effect, Theme};
use unicode_segmentation::UnicodeSegmentation;
use utils::prefix;
use vec::Vec2;
@ -34,9 +32,9 @@ impl<'a> Printer<'a> {
///
/// But nobody needs to know that.
#[doc(hidden)]
pub fn new<T: Into<Vec2>>(size: T, theme: &'a Theme,
backend: &'a backend::Concrete)
-> Self {
pub fn new<T: Into<Vec2>>(
size: T, theme: &'a Theme, backend: &'a backend::Concrete
) -> Self {
Printer {
offset: Vec2::zero(),
size: size.into(),
@ -130,7 +128,8 @@ impl<'a> Printer<'a> {
/// });
/// ```
pub fn with_color<F>(&self, c: ColorStyle, f: F)
where F: FnOnce(&Printer)
where
F: FnOnce(&Printer),
{
self.backend.with_color(c.resolve(self.theme), || f(self));
}
@ -140,7 +139,8 @@ impl<'a> Printer<'a> {
///
/// Will probably use a cursive enum some day.
pub fn with_effect<F>(&self, effect: Effect, f: F)
where F: FnOnce(&Printer)
where
F: FnOnce(&Printer),
{
self.backend.with_effect(effect, || f(self));
}
@ -161,8 +161,9 @@ impl<'a> Printer<'a> {
/// # let printer = Printer::new((6,4), &t, &b);
/// printer.print_box((0,0), (6,4), false);
/// ```
pub fn print_box<T: Into<Vec2>, S: Into<Vec2>>(&self, start: T, size: S,
invert: bool) {
pub fn print_box<T: Into<Vec2>, S: Into<Vec2>>(
&self, start: T, size: S, invert: bool
) {
self.new.set(false);
let start = start.into();
@ -195,7 +196,8 @@ impl<'a> Printer<'a> {
/// use `ColorStyle::Tertiary`.
/// * Otherwise, use `ColorStyle::Primary`.
pub fn with_high_border<F>(&self, invert: bool, f: F)
where F: FnOnce(&Printer)
where
F: FnOnce(&Printer),
{
let color = match self.theme.borders {
BorderStyle::None => return,
@ -213,7 +215,8 @@ impl<'a> Printer<'a> {
/// use `ColorStyle::Tertiary`.
/// * Otherwise, use `ColorStyle::Primary`.
pub fn with_low_border<F>(&self, invert: bool, f: F)
where F: FnOnce(&Printer)
where
F: FnOnce(&Printer),
{
let color = match self.theme.borders {
BorderStyle::None => return,
@ -232,16 +235,18 @@ impl<'a> Printer<'a> {
/// uses `ColorStyle::Highlight`.
/// * Otherwise, uses `ColorStyle::HighlightInactive`.
pub fn with_selection<F: FnOnce(&Printer)>(&self, selection: bool, f: F) {
self.with_color(if selection {
if self.focused {
ColorStyle::Highlight
} else {
ColorStyle::HighlightInactive
}
} else {
ColorStyle::Primary
},
f);
self.with_color(
if selection {
if self.focused {
ColorStyle::Highlight
} else {
ColorStyle::HighlightInactive
}
} else {
ColorStyle::Primary
},
f,
);
}
/// Prints a horizontal delimiter with side border `├` and `┤`.
@ -253,9 +258,9 @@ impl<'a> Printer<'a> {
}
/// Returns a printer on a subset of this one's area.
pub fn sub_printer<S: Into<Vec2>, T: Into<Vec2>>(&'a self, offset: S,
size: T, focused: bool)
-> Printer<'a> {
pub fn sub_printer<S: Into<Vec2>, T: Into<Vec2>>(
&'a self, offset: S, size: T, focused: bool
) -> Printer<'a> {
let size = size.into();
let offset = offset.into().or_min(self.size);
let available = if !offset.fits_in(self.size) {
@ -270,7 +275,7 @@ impl<'a> Printer<'a> {
focused: self.focused && focused,
theme: self.theme,
backend: self.backend,
new: self.new.clone(),
new: Rc::clone(&self.new),
}
}

View File

@ -118,7 +118,6 @@ use std::fs::File;
use std::io;
use std::io::Read;
use std::path::Path;
use toml;
/// Text effect
@ -203,10 +202,9 @@ impl ColorStyle {
pub fn resolve(&self, theme: &Theme) -> ColorPair {
let c = &theme.colors;
let (front, back) = match *self {
ColorStyle::TerminalDefault => (
Color::TerminalDefault,
Color::TerminalDefault,
),
ColorStyle::TerminalDefault => {
(Color::TerminalDefault, Color::TerminalDefault)
}
ColorStyle::Background => (c.view, c.background),
ColorStyle::Shadow => (c.shadow, c.shadow),
ColorStyle::Primary => (c.primary, c.view),
@ -503,7 +501,6 @@ impl Color {
fn parse_special(value: &str) -> Option<Color> {
if value.starts_with('#') {
let value = &value[1..];
// Compute per-color length, and amplitude
let (l, multiplier) = match value.len() {

View File

@ -10,6 +10,5 @@
#[doc(no_inline)]
pub use With;
#[doc(no_inline)]
pub use view::{Boxable, Finder, Identifiable, View};

View File

@ -8,7 +8,8 @@ use std::char::from_u32;
/// Returns an error if the stream is invalid utf-8.
#[allow(dead_code)]
pub fn read_char<F>(first: u8, next: F) -> Result<char, String>
where F: Fn() -> Option<u8>
where
F: Fn() -> Option<u8>,
{
if first < 0x80 {
return Ok(first as char);
@ -32,9 +33,11 @@ pub fn read_char<F>(first: u8, next: F) -> Result<char, String>
let byte =
try!(next().ok_or_else(|| "Missing UTF-8 byte".to_string()));
if byte & 0xC0 != 0x80 {
return Err(format!("Found non-continuation byte after leading: \
{}",
byte));
return Err(format!(
"Found non-continuation byte after leading: \
{}",
byte
));
}
// We have 6 fresh new bits to read, make room.
res <<= 6;

View File

@ -42,9 +42,11 @@ pub struct Prefix {
/// prefix(my_text.graphemes(true), 5, "");
/// # }
/// ```
pub fn prefix<'a, I>(iter: I, available_width: usize, delimiter: &str)
-> Prefix
where I: Iterator<Item = &'a str>
pub fn prefix<'a, I>(
iter: I, available_width: usize, delimiter: &str
) -> Prefix
where
I: Iterator<Item = &'a str>,
{
let delimiter_width = delimiter.width();
let delimiter_len = delimiter.len();
@ -53,17 +55,16 @@ pub fn prefix<'a, I>(iter: I, available_width: usize, delimiter: &str)
// before the next token, including any space.
let mut current_width = 0;
let sum: usize = iter.take_while(|token| {
let width = token.width();
if current_width + width > available_width {
false
} else {
// Include the delimiter after this token.
current_width += width;
current_width += delimiter_width;
true
}
})
.map(|token| token.len() + delimiter_len)
let width = token.width();
if current_width + width > available_width {
false
} else {
// Include the delimiter after this token.
current_width += width;
current_width += delimiter_width;
true
}
}).map(|token| token.len() + delimiter_len)
.sum();
// We counted delimiter once too many times,
@ -88,18 +89,26 @@ pub fn prefix<'a, I>(iter: I, available_width: usize, delimiter: &str)
///
/// This is a shortcut for `prefix_length(iter.rev(), width, delimiter)`
pub fn suffix<'a, I>(iter: I, width: usize, delimiter: &str) -> Prefix
where I: DoubleEndedIterator<Item = &'a str>
where
I: DoubleEndedIterator<Item = &'a str>,
{
prefix(iter.rev(), width, delimiter)
}
/// Computes the length (number of bytes) and width of a suffix that fits in the given `width`.
/// Computes a suffix that fits in the given `width`.
///
/// Breaks between any two graphemes.
pub fn simple_suffix(text: &str, width: usize) -> Prefix {
suffix(text.graphemes(true), width, "")
}
/// Computes the longest prefix of the given text that fits in the given width.
///
/// Breaks between any two graphemes.
pub fn simple_prefix(text: &str, width: usize) -> Prefix {
prefix(text.graphemes(true), width, "")
}
#[cfg(test)]
mod tests {
use utils;

View File

@ -1,5 +1,4 @@
use std::io::{self, Read};
use views::Counter;
/// Wrapper around a `Read` that reports the progress made.

View File

@ -2,8 +2,7 @@
use XY;
use num::traits::Zero;
use std::cmp::{Ordering, max, min};
use std::cmp::{max, min, Ordering};
use std::ops::{Add, Div, Mul, Sub};
/// Simple 2D size, in cells.
@ -15,9 +14,9 @@ use std::ops::{Add, Div, Mul, Sub};
/// [`XY`]: ../struct.XY.html
pub type Vec2 = XY<usize>;
impl PartialOrd for XY<usize> {
impl<T: PartialOrd> PartialOrd for XY<T> {
/// `a < b` <=> `a.x < b.x && a.y < b.y`
fn partial_cmp(&self, other: &Vec2) -> Option<Ordering> {
fn partial_cmp(&self, other: &XY<T>) -> Option<Ordering> {
if self == other {
Some(Ordering::Equal)
} else if self.x < other.x && self.y < other.y {
@ -36,8 +35,10 @@ impl XY<usize> {
/// Never panics.
pub fn saturating_sub<O: Into<Self>>(&self, other: O) -> Self {
let other = other.into();
Self::new(self.x.saturating_sub(other.x),
self.y.saturating_sub(other.y))
Self::new(
self.x.saturating_sub(other.x),
self.y.saturating_sub(other.y),
)
}
/// Checked subtraction. Computes `self - other` if possible.
@ -109,14 +110,28 @@ impl<T: Ord> XY<T> {
impl<T: Ord + Add<Output = T> + Clone> XY<T> {
/// Returns (max(self.x,other.x), self.y+other.y)
pub fn stack_vertical(&self, other: &Self) -> Self {
Self::new(max(self.x.clone(), other.x.clone()),
self.y.clone() + other.y.clone())
Self::new(
max(self.x.clone(), other.x.clone()),
self.y.clone() + other.y.clone(),
)
}
/// Returns (self.x+other.x, max(self.y,other.y))
pub fn stack_horizontal(&self, other: &Self) -> Self {
Self::new(self.x.clone() + other.x.clone(),
max(self.y.clone(), other.y.clone()))
Self::new(
self.x.clone() + other.x.clone(),
max(self.y.clone(), other.y.clone()),
)
}
/// Returns `true` if `self` fits in the given rectangle.
pub fn fits_in_rect<O1, O2>(&self, top_left: O1, size: O2) -> bool
where
O1: Into<Self>,
O2: Into<Self>,
{
let top_left = top_left.into();
self.fits(top_left.clone()) && self < &(top_left + size)
}
}
@ -201,7 +216,7 @@ impl Mul<usize> for XY<usize> {
}
/// Four values representing each direction.
#[derive(Clone,Copy)]
#[derive(Clone, Copy)]
pub struct Vec4 {
/// Left margin
pub left: usize,
@ -268,8 +283,9 @@ impl From<((i32, i32), (i32, i32))> for Vec4 {
}
}
impl From<((usize, usize), (usize, usize))> for Vec4 {
fn from(((left, right), (top, bottom)): ((usize, usize), (usize, usize)))
-> Vec4 {
fn from(
((left, right), (top, bottom)): ((usize, usize), (usize, usize))
) -> Vec4 {
(left, right, top, bottom).into()
}
}

View File

@ -7,8 +7,9 @@ use views::BoxView;
/// [`BoxView`]: ../views/struct.BoxView.html
pub trait Boxable: View + Sized {
/// Wraps `self` in a `BoxView` with the given size constraints.
fn boxed(self, width: SizeConstraint, height: SizeConstraint)
-> BoxView<Self> {
fn boxed(
self, width: SizeConstraint, height: SizeConstraint
) -> BoxView<Self> {
BoxView::new(width, height, self)
}

View File

@ -52,24 +52,19 @@ mod boxable;
pub use self::boxable::Boxable;
pub use self::identifiable::Identifiable;
pub use self::position::{Offset, Position};
pub use self::scroll::{ScrollBase, ScrollStrategy};
pub use self::size_cache::SizeCache;
pub use self::size_constraint::SizeConstraint;
pub use self::view_path::ViewPath;
pub use self::view_wrapper::ViewWrapper;
use Printer;
use direction::Direction;
use event::{Event, EventResult};
use std::any::Any;
use vec::Vec2;
use views::IdView;
use std::any::Any;
/// Main trait defining a view behaviour.
pub trait View {
/// Called when a key was pressed.
@ -161,13 +156,15 @@ pub trait Finder {
/// If the view is not found, or if it is not of the asked type,
/// it returns None.
fn call_on<V, F, R>(&mut self, sel: &Selector, callback: F) -> Option<R>
where V: View + Any,
F: FnOnce(&mut V) -> R;
where
V: View + Any,
F: FnOnce(&mut V) -> R;
/// Convenient method to use `call_on` with a `view::Selector::Id`.
fn find_id<V, F, R>(&mut self, id: &str, callback: F) -> Option<R>
where V: View + Any,
F: FnOnce(&mut V) -> R
where
V: View + Any,
F: FnOnce(&mut V) -> R,
{
self.call_on(&Selector::Id(id), callback)
}
@ -175,8 +172,9 @@ pub trait Finder {
impl<T: View> Finder for T {
fn call_on<V, F, R>(&mut self, sel: &Selector, callback: F) -> Option<R>
where V: View + Any,
F: FnOnce(&mut V) -> R
where
V: View + Any,
F: FnOnce(&mut V) -> R,
{
let mut result = None;
{
@ -184,7 +182,8 @@ impl<T: View> Finder for T {
let mut callback = Some(callback);
let callback = |v: &mut Any| if let Some(callback) =
callback.take() {
callback.take()
{
if v.is::<V>() {
*result_ref = v.downcast_mut::<V>().map(|v| callback(v));
} else if v.is::<IdView<V>>() {

View File

@ -29,23 +29,27 @@ impl Position {
/// and a parent with the absolute coordinates `parent`, drawing the
/// child with its top-left corner at the returned coordinates will
/// position him appropriately.
pub fn compute_offset<S, A, P>(&self, size: S, available: A, parent: P)
-> Vec2
where S: Into<Vec2>,
A: Into<Vec2>,
P: Into<Vec2>
pub fn compute_offset<S, A, P>(
&self, size: S, available: A, parent: P
) -> Vec2
where
S: Into<Vec2>,
A: Into<Vec2>,
P: Into<Vec2>,
{
let available = available.into();
let size = size.into();
let parent = parent.into();
Vec2::new(self.x.compute_offset(size.x, available.x, parent.x),
self.y.compute_offset(size.y, available.y, parent.y))
Vec2::new(
self.x.compute_offset(size.x, available.x, parent.x),
self.y.compute_offset(size.y, available.y, parent.y),
)
}
}
/// Single-dimensional offset policy.
#[derive(PartialEq,Debug,Clone)]
#[derive(PartialEq, Debug, Clone)]
pub enum Offset {
/// In the center of the screen
Center,
@ -60,9 +64,9 @@ pub enum Offset {
impl Offset {
/// Computes a single-dimension offset requred to draw a view.
pub fn compute_offset(&self, size: usize, available: usize,
parent: usize)
-> usize {
pub fn compute_offset(
&self, size: usize, available: usize, parent: usize
) -> usize {
if size > available {
0
} else {

View File

@ -1,6 +1,6 @@
use Printer;
use div::div_up;
use std::cmp::{max, min};
use theme::ColorStyle;
use vec::Vec2;
@ -12,10 +12,13 @@ use vec::Vec2;
pub struct ScrollBase {
/// First line visible
pub start_line: usize,
/// Content height
pub content_height: usize,
/// Number of lines displayed
pub view_height: usize,
/// Padding for the scrollbar
///
/// If present, the scrollbar will be shifted
@ -27,6 +30,9 @@ pub struct ScrollBase {
/// Blank between the text and the scrollbar.
pub right_padding: usize,
/// Initial position of the cursor when dragging.
pub thumb_grab: Option<usize>,
}
/// Defines the scrolling behaviour on content or size change
@ -54,6 +60,7 @@ impl ScrollBase {
view_height: 0,
scrollbar_offset: 0,
right_padding: 1,
thumb_grab: None,
}
}
@ -81,8 +88,8 @@ impl ScrollBase {
self.content_height = content_height;
if self.scrollable() {
self.start_line = min(self.start_line,
self.content_height - self.view_height);
self.start_line =
min(self.start_line, self.content_height - self.view_height);
} else {
self.start_line = 0;
}
@ -129,11 +136,27 @@ impl ScrollBase {
/// Never further than the bottom of the view.
pub fn scroll_down(&mut self, n: usize) {
if self.scrollable() {
self.start_line = min(self.start_line + n,
self.content_height - self.view_height);
self.start_line = min(
self.start_line + n,
self.content_height - self.view_height,
);
}
}
/// Scrolls down until the scrollbar thumb is at the given location.
pub fn scroll_to_thumb(&mut self, thumb_y: usize, thumb_height: usize) {
// The min() is there to stop at the bottom of the content.
// The saturating_sub is there to stop at the bottom of the content.
// eprintln!("Scrolling to {}", thumb_y);
self.start_line = min(
div_up(
(1 + self.content_height - self.view_height) * thumb_y,
(self.view_height - thumb_height + 1),
),
self.content_height - self.view_height,
);
}
/// Scroll up by the given number of lines.
///
/// Never above the top of the view.
@ -143,6 +166,50 @@ impl ScrollBase {
}
}
/// Starts scrolling from the given cursor position.
pub fn start_drag(&mut self, position: Vec2, width: usize) -> bool {
// First: are we on the correct column?
let scrollbar_x = self.scrollbar_x(width);
// eprintln!("Grabbed {} for {}", position.x, scrollbar_x);
if position.x != scrollbar_x {
return false;
}
// Now, did we hit the thumb? Or should we direct-jump?
let height = self.scrollbar_thumb_height();
let thumb_y = self.scrollbar_thumb_y(height);
if position.y >= thumb_y && position.y < thumb_y + height {
// Grabbed!
self.thumb_grab = Some(position.y - thumb_y);
} else {
// Just jump a bit...
self.thumb_grab = Some((height - 1) / 2);
// eprintln!("Grabbed at {}", self.thumb_grab);
self.drag(position);
}
true
}
/// Keeps scrolling by dragging the cursor.
pub fn drag(&mut self, position: Vec2) {
// Our goal is self.scrollbar_thumb_y()+thumb_grab == position.y
// Which means that position.y is the middle of the scrollbar.
// eprintln!("Dragged: {:?}", position);
if let Some(grab) = self.thumb_grab {
let height = self.scrollbar_thumb_height();
self.scroll_to_thumb(position.y.saturating_sub(grab), height);
}
}
/// Stops grabbing the scrollbar.
pub fn release_grab(&mut self) {
self.thumb_grab = None;
}
/// Draws the scroll bar and the content using the given drawer.
///
/// `line_drawer` will be called once for each line that needs to be drawn.
@ -168,14 +235,15 @@ impl ScrollBase {
/// });
/// ```
pub fn draw<F>(&self, printer: &Printer, line_drawer: F)
where F: Fn(&Printer, usize)
where
F: Fn(&Printer, usize),
{
if self.view_height == 0 {
return;
}
// Print the content in a sub_printer
let max_y = min(self.view_height,
self.content_height - self.start_line);
let max_y =
min(self.view_height, self.content_height - self.start_line);
let w = if self.scrollable() {
// We have to remove the bar width and the padding.
printer.size.x.saturating_sub(1 + self.right_padding)
@ -186,10 +254,10 @@ impl ScrollBase {
for y in 0..max_y {
// Y is the actual coordinate of the line.
// The item ID is then Y + self.start_line
line_drawer(&printer.sub_printer(Vec2::new(0, y),
Vec2::new(w, 1),
true),
y + self.start_line);
line_drawer(
&printer.sub_printer(Vec2::new(0, y), Vec2::new(w, 1), true),
y + self.start_line,
);
}
@ -199,15 +267,8 @@ impl ScrollBase {
// (that way we 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);
let height = self.scrollbar_thumb_height();
let start = self.scrollbar_thumb_y(height);
let color = if printer.focused {
ColorStyle::Highlight
@ -215,12 +276,35 @@ impl ScrollBase {
ColorStyle::HighlightInactive
};
// TODO: use 1 instead of 2
let scrollbar_x = printer.size.x.saturating_sub(1 + self.scrollbar_offset);
let scrollbar_x = self.scrollbar_x(printer.size.x);
// eprintln!("Drawing bar at x={}", scrollbar_x);
// The background
printer.print_vline((scrollbar_x, 0), printer.size.y, "|");
// The scrollbar thumb
printer.with_color(color, |printer| {
printer.print_vline((scrollbar_x, start), height, "");
});
}
}
/// Returns the X position of the scrollbar, given the size available.
///
/// Note that this does not depend whether or
/// not a scrollbar will actually be present.
pub fn scrollbar_x(&self, total_size: usize) -> usize {
total_size.saturating_sub(1 + self.scrollbar_offset)
}
/// Returns the height of the scrollbar thumb.
pub fn scrollbar_thumb_height(&self) -> usize {
max(1, self.view_height * self.view_height / self.content_height)
}
/// Returns the y position of the scrollbar thumb.
pub fn scrollbar_thumb_y(&self, scrollbar_thumb_height: usize) -> usize {
let steps = self.view_height - scrollbar_thumb_height + 1;
steps * self.start_line / (1 + self.content_height - self.view_height)
}
}

View File

@ -35,6 +35,11 @@ impl SizeCache {
}
}
/// Returns the value in the cache.
pub fn value(self) -> usize {
self.value
}
/// Creates a new bi-dimensional cache.
///
/// It will stay valid for the same request, and compatible ones.
@ -49,7 +54,9 @@ impl SizeCache {
/// * `size` must fit inside `req`.
/// * for each dimension, `constrained = (size == req)`
pub fn build(size: Vec2, req: Vec2) -> XY<Self> {
XY::new(SizeCache::new(size.x, size.x >= req.x),
SizeCache::new(size.y, size.y >= req.y))
XY::new(
SizeCache::new(size.x, size.x >= req.x),
SizeCache::new(size.y, size.y >= req.y),
)
}
}

View File

@ -29,8 +29,9 @@ impl SizeConstraint {
SizeConstraint::Full |
SizeConstraint::AtLeast(_) => available,
// If the available space is too small, always give in.
SizeConstraint::Fixed(value) |
SizeConstraint::AtMost(value) => min(value, available),
SizeConstraint::Fixed(value) | SizeConstraint::AtMost(value) => {
min(value, available)
}
}
}

View File

@ -27,6 +27,8 @@ pub trait ToPath {
impl<'a> ToPath for &'a [usize] {
fn to_path(self) -> ViewPath {
ViewPath { path: self.to_owned() }
ViewPath {
path: self.to_owned(),
}
}
}

View File

@ -1,5 +1,4 @@
use Printer;
use direction::Direction;
use event::{Event, EventResult};
use std::any::Any;
@ -48,9 +47,8 @@ pub trait ViewWrapper {
/// Wraps the `on_event` method.
fn wrap_on_event(&mut self, ch: Event) -> EventResult {
self.with_view_mut(|v| v.on_event(ch)).unwrap_or(
EventResult::Ignored,
)
self.with_view_mut(|v| v.on_event(ch))
.unwrap_or(EventResult::Ignored)
}
/// Wraps the `layout` method.
@ -60,21 +58,21 @@ pub trait ViewWrapper {
/// Wraps the `take_focus` method.
fn wrap_take_focus(&mut self, source: Direction) -> bool {
self.with_view_mut(|v| v.take_focus(source)).unwrap_or(
false,
)
self.with_view_mut(|v| v.take_focus(source))
.unwrap_or(false)
}
/// Wraps the `find` method.
fn wrap_call_on_any<'a>(&mut self, selector: &Selector, callback: Box<FnMut(&mut Any) + 'a>) {
fn wrap_call_on_any<'a>(
&mut self, selector: &Selector, callback: Box<FnMut(&mut Any) + 'a>
) {
self.with_view_mut(|v| v.call_on_any(selector, callback));
}
/// Wraps the `focus_view` method.
fn wrap_focus_view(&mut self, selector: &Selector) -> Result<(), ()> {
self.with_view_mut(|v| v.focus_view(selector)).unwrap_or(
Err(()),
)
self.with_view_mut(|v| v.focus_view(selector))
.unwrap_or(Err(()))
}
/// Wraps the `needs_relayout` method.
@ -126,7 +124,9 @@ impl<T: ViewWrapper> View for T {
self.wrap_take_focus(source)
}
fn call_on_any<'a>(&mut self, selector: &Selector, callback: Box<FnMut(&mut Any) + 'a>) {
fn call_on_any<'a>(
&mut self, selector: &Selector, callback: Box<FnMut(&mut Any) + 'a>
) {
self.wrap_call_on_any(selector, callback)
}

View File

@ -38,8 +38,9 @@ impl<T: View> BoxView<T> {
/// Creates a new `BoxView` with the given width and height requirements.
///
/// `None` values will use the wrapped view's preferences.
pub fn new(width: SizeConstraint, height: SizeConstraint, view: T)
-> Self {
pub fn new(
width: SizeConstraint, height: SizeConstraint, view: T
) -> Self {
BoxView {
size: (width, height).into(),
squishable: false,
@ -64,9 +65,11 @@ impl<T: View> BoxView<T> {
pub fn with_fixed_size<S: Into<Vec2>>(size: S, view: T) -> Self {
let size = size.into();
BoxView::new(SizeConstraint::Fixed(size.x),
SizeConstraint::Fixed(size.y),
view)
BoxView::new(
SizeConstraint::Fixed(size.x),
SizeConstraint::Fixed(size.y),
view,
)
}
/// Wraps `view` in a new `BoxView` with fixed width.
@ -98,54 +101,66 @@ impl<T: View> BoxView<T> {
pub fn with_max_size<S: Into<Vec2>>(size: S, view: T) -> Self {
let size = size.into();
BoxView::new(SizeConstraint::AtMost(size.x),
SizeConstraint::AtMost(size.y),
view)
BoxView::new(
SizeConstraint::AtMost(size.x),
SizeConstraint::AtMost(size.y),
view,
)
}
/// Wraps `view` in a `BoxView` which will enforce a maximum width.
///
/// The resulting width will never be more than `max_width`.
pub fn with_max_width(max_width: usize, view: T) -> Self {
BoxView::new(SizeConstraint::AtMost(max_width),
SizeConstraint::Free,
view)
BoxView::new(
SizeConstraint::AtMost(max_width),
SizeConstraint::Free,
view,
)
}
/// Wraps `view` in a `BoxView` which will enforce a maximum height.
///
/// The resulting height will never be more than `max_height`.
pub fn with_max_height(max_height: usize, view: T) -> Self {
BoxView::new(SizeConstraint::Free,
SizeConstraint::AtMost(max_height),
view)
BoxView::new(
SizeConstraint::Free,
SizeConstraint::AtMost(max_height),
view,
)
}
/// Wraps `view` in a `BoxView` which will never be smaller than `size`.
pub fn with_min_size<S: Into<Vec2>>(size: S, view: T) -> Self {
let size = size.into();
BoxView::new(SizeConstraint::AtLeast(size.x),
SizeConstraint::AtLeast(size.y),
view)
BoxView::new(
SizeConstraint::AtLeast(size.x),
SizeConstraint::AtLeast(size.y),
view,
)
}
/// Wraps `view` in a `BoxView` which will enforce a minimum width.
///
/// The resulting width will never be less than `min_width`.
pub fn with_min_width(min_width: usize, view: T) -> Self {
BoxView::new(SizeConstraint::AtLeast(min_width),
SizeConstraint::Free,
view)
BoxView::new(
SizeConstraint::AtLeast(min_width),
SizeConstraint::Free,
view,
)
}
/// Wraps `view` in a `BoxView` which will enforce a minimum height.
///
/// The resulting height will never be less than `min_height`.
pub fn with_min_height(min_height: usize, view: T) -> Self {
BoxView::new(SizeConstraint::Free,
SizeConstraint::AtLeast(min_height),
view)
BoxView::new(
SizeConstraint::Free,
SizeConstraint::AtLeast(min_height),
view,
)
}
}
@ -153,7 +168,6 @@ impl<T: View> ViewWrapper for BoxView<T> {
wrap_impl!(self.view: T);
fn wrap_required_size(&mut self, req: Vec2) -> Vec2 {
let req = self.size.zip_map(req, SizeConstraint::available);
let child_size = self.view.required_size(req);
@ -165,16 +179,18 @@ impl<T: View> ViewWrapper for BoxView<T> {
if self.squishable {
// We respect the request if we're less or equal.
let respect_req = result.zip_map(req, |res, req| res <= req);
result.zip_map(respect_req.zip(child_size),
|res, (respect, child)| {
if respect {
// If we respect the request, keep the result
res
} else {
// Otherwise, take the child as squish attempt.
child
}
})
result.zip_map(
respect_req.zip(child_size),
|res, (respect, child)| {
if respect {
// If we respect the request, keep the result
res
} else {
// Otherwise, take the child as squish attempt.
child
}
},
)
} else {
result
}

View File

@ -28,7 +28,8 @@ pub struct Button {
impl Button {
/// Creates a new button with the given content and callback.
pub fn new<F, S: Into<String>>(label: S, cb: F) -> Self
where F: Fn(&mut Cursive) + 'static
where
F: Fn(&mut Cursive) + 'static,
{
Button {
label: label.into(),
@ -41,7 +42,8 @@ impl Button {
///
/// Replaces the previous callback.
pub fn set_callback<F>(&mut self, cb: F)
where F: Fn(&mut Cursive) + 'static
where
F: Fn(&mut Cursive) + 'static,
{
self.callback = Callback::from_fn(cb);
}
@ -74,11 +76,14 @@ impl Button {
pub fn is_enabled(&self) -> bool {
self.enabled
}
fn req_size(&self) -> Vec2 {
Vec2::new(2 + self.label.width(), 1)
}
}
impl View for Button {
fn draw(&self, printer: &Printer) {
if printer.size.x == 0 {
return;
}
@ -101,15 +106,25 @@ impl View for Button {
fn required_size(&mut self, _: Vec2) -> Vec2 {
// Meh. Fixed size we are.
Vec2::new(2 + self.label.width(), 1)
self.req_size()
}
fn on_event(&mut self, event: Event) -> EventResult {
// eprintln!("{:?}", event);
// eprintln!("{:?}", self.req_size());
match event {
// 10 is the ascii code for '\n', that is the return key
Event::Key(Key::Enter) => {
EventResult::Consumed(Some(self.callback.clone()))
}
Event::Mouse {
event: MouseEvent::Release(MouseButton::Left),
position,
offset,
} if position.fits_in_rect(offset, self.req_size()) =>
{
EventResult::Consumed(Some(self.callback.clone()))
}
_ => EventResult::Ignored,
}
}

View File

@ -19,7 +19,7 @@ pub struct Canvas<T> {
needs_relayout: Box<Fn(&T) -> bool>,
}
impl <T: 'static + View> Canvas<T> {
impl<T: 'static + View> Canvas<T> {
/// Creates a new Canvas around the given view.
///
/// By default, forwards all calls to the inner view.
@ -65,7 +65,8 @@ impl<T> Canvas<T> {
/// Sets the closure for `draw(&Printer)`.
pub fn set_draw<F>(&mut self, f: F)
where F: 'static + Fn(&T, &Printer)
where
F: 'static + Fn(&T, &Printer),
{
self.draw = Box::new(f);
}
@ -74,14 +75,16 @@ impl<T> Canvas<T> {
///
/// Chainable variant.
pub fn with_draw<F>(self, f: F) -> Self
where F: 'static + Fn(&T, &Printer)
where
F: 'static + Fn(&T, &Printer),
{
self.with(|s| s.set_draw(f))
}
/// Sets the closure for `on_event(Event)`.
pub fn set_on_event<F>(&mut self, f: F)
where F: 'static + FnMut(&mut T, Event) -> EventResult
where
F: 'static + FnMut(&mut T, Event) -> EventResult,
{
self.on_event = Box::new(f);
}
@ -90,14 +93,16 @@ impl<T> Canvas<T> {
///
/// Chainable variant.
pub fn with_on_event<F>(self, f: F) -> Self
where F: 'static + FnMut(&mut T, Event) -> EventResult
where
F: 'static + FnMut(&mut T, Event) -> EventResult,
{
self.with(|s| s.set_on_event(f))
}
/// Sets the closure for `required_size(Vec2)`.
pub fn set_required_size<F>(&mut self, f: F)
where F: 'static + FnMut(&mut T, Vec2) -> Vec2
where
F: 'static + FnMut(&mut T, Vec2) -> Vec2,
{
self.required_size = Box::new(f);
}
@ -106,14 +111,16 @@ impl<T> Canvas<T> {
///
/// Chainable variant.
pub fn with_required_size<F>(self, f: F) -> Self
where F: 'static + FnMut(&mut T, Vec2) -> Vec2
where
F: 'static + FnMut(&mut T, Vec2) -> Vec2,
{
self.with(|s| s.set_required_size(f))
}
/// Sets the closure for `layout(Vec2)`.
pub fn set_layout<F>(&mut self, f: F)
where F: 'static + FnMut(&mut T, Vec2)
where
F: 'static + FnMut(&mut T, Vec2),
{
self.layout = Box::new(f);
}
@ -122,14 +129,16 @@ impl<T> Canvas<T> {
///
/// Chainable variant.
pub fn with_layout<F>(self, f: F) -> Self
where F: 'static + FnMut(&mut T, Vec2)
where
F: 'static + FnMut(&mut T, Vec2),
{
self.with(|s| s.set_layout(f))
}
/// Sets the closure for `take_focus(Direction)`.
pub fn set_take_focus<F>(&mut self, f: F)
where F: 'static + FnMut(&mut T, Direction) -> bool
where
F: 'static + FnMut(&mut T, Direction) -> bool,
{
self.take_focus = Box::new(f);
}
@ -138,14 +147,16 @@ impl<T> Canvas<T> {
///
/// Chainable variant.
pub fn with_take_focus<F>(self, f: F) -> Self
where F: 'static + FnMut(&mut T, Direction) -> bool
where
F: 'static + FnMut(&mut T, Direction) -> bool,
{
self.with(|s| s.set_take_focus(f))
}
/// Sets the closure for `needs_relayout()`.
pub fn set_needs_relayout<F>(&mut self, f: F)
where F: 'static + Fn(&T) -> bool
where
F: 'static + Fn(&T) -> bool,
{
self.needs_relayout = Box::new(f);
}
@ -155,7 +166,8 @@ impl<T> Canvas<T> {
///
/// Chainable variant.
pub fn with_needs_relayout<F>(self, f: F) -> Self
where F: 'static + Fn(&T) -> bool
where
F: 'static + Fn(&T) -> bool,
{
self.with(|s| s.set_needs_relayout(f))
}

View File

@ -2,8 +2,7 @@ use Cursive;
use Printer;
use With;
use direction::Direction;
use event::{Event, EventResult, Key};
use event::{Event, EventResult, Key, MouseButton, MouseEvent};
use std::rc::Rc;
use theme::ColorStyle;
use vec::Vec2;
@ -33,16 +32,18 @@ impl Checkbox {
}
/// Sets a callback to be used when the state changes.
pub fn set_on_change<F: 'static + Fn(&mut Cursive, bool)>(&mut self,
on_change: F) {
pub fn set_on_change<F: 'static + Fn(&mut Cursive, bool)>(
&mut self, on_change: F
) {
self.on_change = Some(Rc::new(on_change));
}
/// Sets a callback to be used when the state changes.
///
/// Chainable variant.
pub fn on_change<F: 'static + Fn(&mut Cursive, bool)>(self, on_change: F)
-> Self {
pub fn on_change<F: 'static + Fn(&mut Cursive, bool)>(
self, on_change: F
) -> Self {
self.with(|s| s.set_on_change(on_change))
}
@ -61,7 +62,9 @@ impl Checkbox {
///
/// Chainable variant.
pub fn checked(self) -> Self {
self.with(|s| { s.check(); })
self.with(|s| {
s.check();
})
}
/// Returns `true` if the checkbox is checked.
@ -78,14 +81,16 @@ impl Checkbox {
///
/// Chainable variant.
pub fn unchecked(self) -> Self {
self.with(|s| { s.uncheck(); })
self.with(|s| {
s.uncheck();
})
}
/// Sets the checkbox state.
pub fn set_checked(&mut self, checked: bool) -> EventResult {
self.checked = checked;
if let Some(ref on_change) = self.on_change {
let on_change = on_change.clone();
let on_change = Rc::clone(on_change);
EventResult::with_cb(move |s| on_change(s, checked))
} else {
EventResult::Consumed(None)
@ -111,19 +116,29 @@ impl View for Checkbox {
fn draw(&self, printer: &Printer) {
if self.enabled {
printer.with_selection(printer.focused,
|printer| self.draw_internal(printer));
printer.with_selection(
printer.focused,
|printer| self.draw_internal(printer),
);
} else {
printer.with_color(ColorStyle::Secondary,
|printer| self.draw_internal(printer));
printer.with_color(
ColorStyle::Secondary,
|printer| self.draw_internal(printer),
);
}
}
fn on_event(&mut self, event: Event) -> EventResult {
match event {
Event::Key(Key::Enter) |
Event::Char(' ') => self.toggle(),
Event::Key(Key::Enter) | Event::Char(' ') => self.toggle(),
Event::Mouse {
event: MouseEvent::Release(MouseButton::Left),
position,
offset,
} if position.fits_in_rect(offset, (3, 1)) =>
{
self.toggle()
}
_ => EventResult::Ignored,
}
}

View File

@ -1,5 +1,3 @@
use Cursive;
use Printer;
use With;
@ -7,9 +5,9 @@ use align::*;
use direction::Direction;
use event::*;
use std::any::Any;
use std::cell::Cell;
use std::cmp::max;
use theme::ColorStyle;
use unicode_width::UnicodeWidthStr;
use vec::{Vec2, Vec4};
use view::{Selector, View};
@ -21,6 +19,23 @@ enum Focus {
Button(usize),
}
struct ChildButton {
button: SizedView<Button>,
offset: Cell<Vec2>,
}
impl ChildButton {
pub fn new<F, S: Into<String>>(label: S, cb: F) -> Self
where
F: Fn(&mut Cursive) + 'static,
{
ChildButton {
button: SizedView::new(Button::new(label, cb)),
offset: Cell::new(Vec2::zero()),
}
}
}
/// Popup-like view with a main content, and optional buttons under it.
///
/// # Examples
@ -31,16 +46,26 @@ enum Focus {
/// .button("Ok", |s| s.quit());
/// ```
pub struct Dialog {
// Possibly empty title.
title: String,
content: Box<View>,
buttons: Vec<SizedView<Button>>,
// The actual inner view.
content: SizedView<Box<View>>,
// Optional list of buttons under the main view.
// Include the top-left corner.
buttons: Vec<ChildButton>,
// Padding around the inner view.
padding: Vec4,
// Borders around everything.
borders: Vec4,
// The current element in focus
focus: Focus,
// How to align the buttons under the view.
align: Align,
}
@ -57,7 +82,7 @@ impl Dialog {
/// Creates a new `Dialog` with the given content.
pub fn around<V: View + 'static>(view: V) -> Self {
Dialog {
content: Box::new(view),
content: SizedView::new(Box::new(view)),
buttons: Vec::new(),
title: String::new(),
focus: Focus::Content,
@ -78,7 +103,7 @@ impl Dialog {
///
/// Previous content will be dropped.
pub fn set_content<V: View + 'static>(&mut self, view: V) {
self.content = Box::new(view);
self.content = SizedView::new(Box::new(view));
}
/// Convenient method to create a dialog with a simple text content.
@ -97,9 +122,10 @@ impl Dialog {
///
/// Consumes and returns self for easy chaining.
pub fn button<F, S: Into<String>>(mut self, label: S, cb: F) -> Self
where F: Fn(&mut Cursive) + 'static
where
F: Fn(&mut Cursive) + 'static,
{
self.buttons.push(SizedView::new(Button::new(label, cb)));
self.buttons.push(ChildButton::new(label, cb));
self
}
@ -113,6 +139,9 @@ impl Dialog {
self
}
/*
* Commented out because currently un-implemented.
*
/// Sets the vertical alignment for the buttons, if any.
///
/// Only works if the buttons are as a column to the right of the dialog.
@ -121,6 +150,7 @@ impl Dialog {
self
}
*/
/// Shortcut method to add a button that will dismiss the dialog.
pub fn dismiss_button<S: Into<String>>(self, label: S) -> Self {
@ -169,65 +199,150 @@ impl Dialog {
self.padding.right = padding;
self
}
}
impl View for Dialog {
fn draw(&self, printer: &Printer) {
// This will be the buttons_height used by the buttons.
// Private methods
// An event is received while the content is in focus
fn on_event_content(&mut self, event: Event) -> EventResult {
match self.content.on_event(
event.relativized((self.padding + self.borders).top_left()),
) {
EventResult::Ignored if !self.buttons.is_empty() => {
match event {
Event::Key(Key::Down) |
Event::Key(Key::Tab) |
Event::Shift(Key::Tab) => {
// Default to leftmost button when going down.
self.focus = Focus::Button(0);
EventResult::Consumed(None)
}
_ => EventResult::Ignored,
}
}
res => res,
}
}
// An event is received while a button is in focus
fn on_event_button(
&mut self, event: Event, button_id: usize
) -> EventResult {
let result = {
let button = &mut self.buttons[button_id];
button
.button
.on_event(event.relativized(button.offset.get()))
};
match result {
EventResult::Ignored => {
match event {
// Up goes back to the content
Event::Key(Key::Up) => {
if self.content.take_focus(Direction::down()) {
self.focus = Focus::Content;
EventResult::Consumed(None)
} else {
EventResult::Ignored
}
}
Event::Shift(Key::Tab) => {
if self.content.take_focus(Direction::back()) {
self.focus = Focus::Content;
EventResult::Consumed(None)
} else {
EventResult::Ignored
}
}
Event::Key(Key::Tab) => {
if self.content.take_focus(Direction::front()) {
self.focus = Focus::Content;
EventResult::Consumed(None)
} else {
EventResult::Ignored
}
}
// Left and Right move to other buttons
Event::Key(Key::Right)
if button_id + 1 < self.buttons.len() =>
{
self.focus = Focus::Button(button_id + 1);
EventResult::Consumed(None)
}
Event::Key(Key::Left) if button_id > 0 => {
self.focus = Focus::Button(button_id - 1);
EventResult::Consumed(None)
}
_ => EventResult::Ignored,
}
}
res => res,
}
}
fn draw_buttons(&self, printer: &Printer) -> Option<usize> {
let mut buttons_height = 0;
// Current horizontal position of the next button we'll draw.
// Sum of the sizes + len-1 for margins
let width = self.buttons
.iter()
.map(|button| button.size.x)
.fold(0, |a, b| a + b) +
self.buttons.len().saturating_sub(1);
.map(|button| button.button.size.x)
.fold(0, |a, b| a + b)
+ self.buttons.len().saturating_sub(1);
let overhead = self.padding + self.borders;
if printer.size.x < overhead.horizontal() {
return;
return None;
}
let mut offset = overhead.left +
self.align.h.get_offset(width,
printer.size.x -
overhead.horizontal());
let mut offset = overhead.left
+ self.align
.h
.get_offset(width, printer.size.x - overhead.horizontal());
let overhead_bottom = self.padding.bottom + self.borders.bottom + 1;
let y = match printer.size.y.checked_sub(overhead_bottom) {
Some(y) => y,
None => return,
None => return None,
};
for (i, button) in self.buttons.iter().enumerate() {
let size = button.size;
let size = button.button.size;
// Add some special effect to the focused button
button.draw(&printer.sub_printer(Vec2::new(offset, y),
size,
self.focus == Focus::Button(i)));
let position = Vec2::new(offset, y);
button.offset.set(position);
button.button.draw(&printer.sub_printer(
position,
size,
self.focus == Focus::Button(i),
));
// Keep 1 blank between two buttons
offset += size.x + 1;
// Also keep 1 blank above the buttons
buttons_height = max(buttons_height, size.y + 1);
}
Some(buttons_height)
}
fn draw_content(&self, printer: &Printer, buttons_height: usize) {
// What do we have left?
let taken = Vec2::new(0, buttons_height) + self.borders.combined() +
self.padding.combined();
let taken = Vec2::new(0, buttons_height) + self.borders.combined()
+ self.padding.combined();
let inner_size = match printer.size.checked_sub(taken) {
Some(s) => s,
None => return,
};
self.content.draw(&printer.sub_printer(self.borders.top_left() +
self.padding.top_left(),
inner_size,
self.focus == Focus::Content));
printer.print_box(Vec2::new(0, 0), printer.size, false);
self.content.draw(&printer.sub_printer(
self.borders.top_left() + self.padding.top_left(),
inner_size,
self.focus == Focus::Content,
));
}
fn draw_title(&self, printer: &Printer) {
if !self.title.is_empty() {
let len = self.title.width();
if len + 4 > printer.size.x {
@ -239,10 +354,64 @@ impl View for Dialog {
printer.print((x + len, 0), "");
});
printer.with_color(ColorStyle::TitlePrimary,
|p| p.print((x, 0), &self.title));
printer.with_color(
ColorStyle::TitlePrimary,
|p| p.print((x, 0), &self.title),
);
}
}
fn check_focus_grab(&mut self, event: &Event) {
if let Event::Mouse {
offset,
position,
event,
} = *event
{
if !event.grabs_focus() {
return;
}
let position = match position.checked_sub(offset) {
None => return,
Some(pos) => pos,
};
// eprintln!("Rel pos: {:?}", position);
// Now that we have a relative position, checks for buttons?
if let Some(i) = self.buttons.iter().position(|btn| {
// If position fits there...
position.fits_in_rect(btn.offset.get(), btn.button.size)
}) {
self.focus = Focus::Button(i);
} else if position.fits_in_rect(
(self.padding + self.borders).top_left(),
self.content.size,
)
&& self.content.take_focus(Direction::none())
{
// Or did we click the content?
self.focus = Focus::Content;
}
}
}
}
impl View for Dialog {
fn draw(&self, printer: &Printer) {
// This will be the buttons_height used by the buttons.
let buttons_height = match self.draw_buttons(printer) {
Some(height) => height,
None => return,
};
self.draw_content(printer, buttons_height);
// Print the borders
printer.print_box(Vec2::new(0, 0), printer.size, false);
self.draw_title(printer);
}
fn required_size(&mut self, req: Vec2) -> Vec2 {
@ -256,7 +425,7 @@ impl View for Dialog {
buttons_size.x += self.buttons.len().saturating_sub(1);
for button in &mut self.buttons {
let s = button.view.required_size(req);
let s = button.button.view.required_size(req);
buttons_size.x += s.x;
buttons_size.y = max(buttons_size.y, s.y + 1);
}
@ -274,10 +443,11 @@ impl View for Dialog {
// On the Y axis, we add buttons and content.
// On the X axis, we take the max.
let mut inner_size = Vec2::new(max(content_size.x, buttons_size.x),
content_size.y + buttons_size.y) +
self.padding.combined() +
self.borders.combined();
let mut inner_size = Vec2::new(
max(content_size.x, buttons_size.x),
content_size.y + buttons_size.y,
) + self.padding.combined()
+ self.borders.combined();
if !self.title.is_empty() {
// If we have a title, we have to fit it too!
@ -296,85 +466,29 @@ impl View for Dialog {
// Buttons are kings, we give them everything they want.
let mut buttons_height = 0;
for button in self.buttons.iter_mut().rev() {
let size = button.required_size(size);
let size = button.button.required_size(size);
buttons_height = max(buttons_height, size.y + 1);
button.layout(size);
button.button.layout(size);
}
// Poor content will have to make do with what's left.
if buttons_height > size.y {
buttons_height = size.y;
}
self.content.layout(size.saturating_sub((0, buttons_height)));
self.content
.layout(size.saturating_sub((0, buttons_height)));
}
fn on_event(&mut self, event: Event) -> EventResult {
// First: some mouse events can instantly change the focus.
self.check_focus_grab(&event);
match self.focus {
// If we are on the content, we can only go down.
Focus::Content => {
match self.content.on_event(event.clone()) {
EventResult::Ignored if !self.buttons.is_empty() => {
match event {
Event::Key(Key::Down) |
Event::Key(Key::Tab) |
Event::Shift(Key::Tab) => {
// Default to leftmost button when going down.
self.focus = Focus::Button(0);
EventResult::Consumed(None)
}
_ => EventResult::Ignored,
}
}
res => res,
}
}
// TODO: Careful if/when we add buttons elsewhere on the dialog!
Focus::Content => self.on_event_content(event),
// If we are on a button, we have more choice
Focus::Button(i) => {
match self.buttons[i].on_event(event.clone()) {
EventResult::Ignored => {
match event {
// Up goes back to the content
Event::Key(Key::Up) => {
if self.content.take_focus(Direction::down()) {
self.focus = Focus::Content;
EventResult::Consumed(None)
} else {
EventResult::Ignored
}
}
Event::Shift(Key::Tab) => {
if self.content.take_focus(Direction::back()) {
self.focus = Focus::Content;
EventResult::Consumed(None)
} else {
EventResult::Ignored
}
}
Event::Key(Key::Tab) => {
if self.content
.take_focus(Direction::front()) {
self.focus = Focus::Content;
EventResult::Consumed(None)
} else {
EventResult::Ignored
}
}
// Left and Right move to other buttons
Event::Key(Key::Right) if i + 1 <
self.buttons.len() => {
self.focus = Focus::Button(i + 1);
EventResult::Consumed(None)
}
Event::Key(Key::Left) if i > 0 => {
self.focus = Focus::Button(i - 1);
EventResult::Consumed(None)
}
_ => EventResult::Ignored,
}
}
res => res,
}
}
Focus::Button(i) => self.on_event_button(event, i),
}
}
@ -392,8 +506,9 @@ impl View for Dialog {
}
}
fn call_on_any<'a>(&mut self, selector: &Selector,
callback: Box<FnMut(&mut Any) + 'a>) {
fn call_on_any<'a>(
&mut self, selector: &Selector, callback: Box<FnMut(&mut Any) + 'a>
) {
self.content.call_on_any(selector, callback);
}

View File

@ -2,14 +2,13 @@
use {Cursive, Printer, With};
use direction::Direction;
use event::{Callback, Event, EventResult, Key};
use event::{Callback, Event, EventResult, Key, MouseEvent};
use std::cell::RefCell;
use std::rc::Rc;
use theme::{ColorStyle, Effect};
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
use utils::simple_suffix;
use utils::{simple_prefix, simple_suffix};
use vec::Vec2;
use view::View;
@ -202,7 +201,8 @@ impl EditView {
/// If you don't need a mutable closure but want the possibility of
/// recursive calls, see [`set_on_edit`](#method.set_on_edit).
pub fn set_on_edit_mut<F>(&mut self, callback: F)
where F: FnMut(&mut Cursive, &str, usize) + 'static
where
F: FnMut(&mut Cursive, &str, usize) + 'static,
{
let callback = RefCell::new(callback);
// Here's the weird trick: if we're already borrowed,
@ -227,7 +227,8 @@ impl EditView {
/// If you need a mutable closure and don't care about the recursive
/// aspect, see [`set_on_edit_mut`](#method.set_on_edit_mut).
pub fn set_on_edit<F>(&mut self, callback: F)
where F: Fn(&mut Cursive, &str, usize) + 'static
where
F: Fn(&mut Cursive, &str, usize) + 'static,
{
self.on_edit = Some(Rc::new(callback));
}
@ -236,7 +237,8 @@ impl EditView {
///
/// Chainable variant. See [`set_on_edit_mut`](#method.set_on_edit_mut).
pub fn on_edit_mut<F>(self, callback: F) -> Self
where F: FnMut(&mut Cursive, &str, usize) + 'static
where
F: FnMut(&mut Cursive, &str, usize) + 'static,
{
self.with(|v| v.set_on_edit_mut(callback))
}
@ -245,7 +247,8 @@ impl EditView {
///
/// Chainable variant. See [`set_on_edit`](#method.set_on_edit).
pub fn on_edit<F>(self, callback: F) -> Self
where F: Fn(&mut Cursive, &str, usize) + 'static
where
F: Fn(&mut Cursive, &str, usize) + 'static,
{
self.with(|v| v.set_on_edit(callback))
}
@ -260,16 +263,18 @@ impl EditView {
/// If you don't need a mutable closure but want the possibility of
/// recursive calls, see [`set_on_submit`](#method.set_on_submit).
pub fn set_on_submit_mut<F>(&mut self, callback: F)
where F: FnMut(&mut Cursive, &str) + 'static
where
F: FnMut(&mut Cursive, &str) + 'static,
{
// TODO: don't duplicate all those methods.
// Instead, have some generic function immutify()
// or something that wraps a FnMut closure.
let callback = RefCell::new(callback);
self.set_on_submit(move |s, text| if let Ok(mut f) =
callback.try_borrow_mut() {
(&mut *f)(s, text);
});
self.set_on_submit(
move |s, text| if let Ok(mut f) = callback.try_borrow_mut() {
(&mut *f)(s, text);
},
);
}
/// Sets a callback to be called when `<Enter>` is pressed.
@ -282,7 +287,8 @@ impl EditView {
/// If you need a mutable closure and don't care about the recursive
/// aspect, see [`set_on_submit_mut`](#method.set_on_submit_mut).
pub fn set_on_submit<F>(&mut self, callback: F)
where F: Fn(&mut Cursive, &str) + 'static
where
F: Fn(&mut Cursive, &str) + 'static,
{
self.on_submit = Some(Rc::new(callback));
}
@ -291,7 +297,8 @@ impl EditView {
///
/// Chainable variant.
pub fn on_submit_mut<F>(self, callback: F) -> Self
where F: FnMut(&mut Cursive, &str) + 'static
where
F: FnMut(&mut Cursive, &str) + 'static,
{
self.with(|v| v.set_on_submit_mut(callback))
}
@ -300,7 +307,8 @@ impl EditView {
///
/// Chainable variant.
pub fn on_submit<F>(self, callback: F) -> Self
where F: Fn(&mut Cursive, &str) + 'static
where
F: Fn(&mut Cursive, &str) + 'static,
{
self.with(|v| v.set_on_submit(callback))
}
@ -327,7 +335,7 @@ impl EditView {
/// Get the current text.
pub fn get_content(&self) -> Rc<String> {
self.content.clone()
Rc::clone(&self.content)
}
/// Sets the current content to the given value.
@ -388,16 +396,15 @@ impl EditView {
// Look at the content before the cursor (we will print its tail).
// From the end, count the length until we reach `available`.
// Then sum the byte lengths.
let suffix_length = simple_suffix(&self.content[self.offset..
self.cursor],
available)
.length;
let suffix_length = simple_suffix(
&self.content[self.offset..self.cursor],
available,
).length;
assert!(suffix_length <= self.cursor);
self.offset = self.cursor - suffix_length;
// Make sure the cursor is in view
assert!(self.cursor >= self.offset);
}
// If we have too much space
@ -423,10 +430,13 @@ fn make_small_stars(length: usize) -> &'static str {
impl View for EditView {
fn draw(&self, printer: &Printer) {
assert_eq!(printer.size.x, self.last_length,
"Was promised {}, received {}",
self.last_length,
printer.size.x);
assert_eq!(
printer.size.x,
self.last_length,
"Was promised {}, received {}",
self.last_length,
printer.size.x
);
let width = self.content.width();
printer.with_color(self.style, |printer| {
@ -444,16 +454,24 @@ impl View for EditView {
} else {
printer.print((0, 0), &self.content);
}
let filler_len = (printer.size.x - width) / self.filler.width();
printer.print_hline((width, 0),
filler_len,
self.filler.as_str());
let filler_len =
(printer.size.x - width) / self.filler.width();
printer.print_hline(
(width, 0),
filler_len,
self.filler.as_str(),
);
} else {
let content = &self.content[self.offset..];
let display_bytes = content.graphemes(true)
let display_bytes = content
.graphemes(true)
.scan(0, |w, g| {
*w += g.width();
if *w > self.last_length { None } else { Some(g) }
if *w > self.last_length {
None
} else {
Some(g)
}
})
.map(|g| g.len())
.fold(0, |a, b| a + b);
@ -468,10 +486,13 @@ impl View for EditView {
}
if width < self.last_length {
let filler_len = (self.last_length - width) / self.filler.width();
printer.print_hline((width, 0),
filler_len,
self.filler.as_str());
let filler_len =
(self.last_length - width) / self.filler.width();
printer.print_hline(
(width, 0),
filler_len,
self.filler.as_str(),
);
}
}
});
@ -485,9 +506,11 @@ impl View for EditView {
let selected = self.content[self.cursor..]
.graphemes(true)
.next()
.expect(&format!("Found no char at cursor {} in {}",
self.cursor,
&self.content));
.expect(&format!(
"Found no char at cursor {} in {}",
self.cursor,
&self.content
));
if self.secret {
make_small_stars(selected.width())
} else {
@ -510,7 +533,6 @@ impl View for EditView {
}
fn on_event(&mut self, event: Event) -> EventResult {
match event {
Event::Char(ch) => self.insert(ch),
// TODO: handle ctrl-key?
@ -551,8 +573,24 @@ impl View for EditView {
}
Event::Key(Key::Enter) if self.on_submit.is_some() => {
let cb = self.on_submit.clone().unwrap();
let content = self.content.clone();
return EventResult::with_cb(move |s| { cb(s, &content); });
let content = Rc::clone(&self.content);
return EventResult::with_cb(move |s| {
cb(s, &content);
});
}
Event::Mouse {
event: MouseEvent::Press(_),
position,
offset,
} if position.fits_in_rect(offset, (self.last_length, 1)) =>
{
position.checked_sub(offset).map(|position| {
self.cursor = self.offset
+ simple_prefix(
&self.content[self.offset..],
position.x,
).length;
});
}
_ => return EventResult::Ignored,
}
@ -560,12 +598,13 @@ impl View for EditView {
self.keep_cursor_in_view();
let cb = self.on_edit.clone().map(|cb| {
// Get a new Rc on the content
let content = self.content.clone();
let content = Rc::clone(&self.content);
let cursor = self.cursor;
Callback::from_fn(move |s| { cb(s, &content, cursor); })
Callback::from_fn(move |s| {
cb(s, &content, cursor);
})
});
EventResult::Consumed(cb)
}

View File

@ -1,6 +1,5 @@
use owning_ref::{RcRef, OwningHandle};
use owning_ref::{OwningHandle, RcRef};
use std::any::Any;
use std::cell::{RefCell, RefMut};
use std::rc::Rc;
use view::{Selector, View, ViewWrapper};
@ -35,7 +34,7 @@ impl<V: View> IdView<V> {
///
/// Panics if another reference for this view already exists.
pub fn get_mut(&mut self) -> ViewRef<V> {
let cell_ref = RcRef::new(self.view.clone());
let cell_ref = RcRef::new(Rc::clone(&self.view));
OwningHandle::new_mut(cell_ref)
}
@ -58,16 +57,18 @@ impl<T: View + 'static> ViewWrapper for IdView<T> {
self.view.try_borrow_mut().ok().map(|mut v| f(&mut *v))
}
// Some for<'b> weirdness here to please the borrow checker gods...
fn wrap_call_on_any<'a>(
&mut self, selector: &Selector,
mut callback: Box<for<'b> FnMut(&'b mut Any) + 'a>
mut callback: Box<for<'b> FnMut(&'b mut Any) + 'a>,
) {
match selector {
&Selector::Id(id) if id == self.id => callback(self),
s => {
self.view.try_borrow_mut().ok().map(|mut v| {
v.call_on_any(s, callback)
});
self.view
.try_borrow_mut()
.ok()
.map(|mut v| v.call_on_any(s, callback));
}
}
}
@ -75,13 +76,10 @@ impl<T: View + 'static> ViewWrapper for IdView<T> {
fn wrap_focus_view(&mut self, selector: &Selector) -> Result<(), ()> {
match selector {
&Selector::Id(id) if id == self.id => Ok(()),
s => {
self.view.try_borrow_mut().map_err(|_| ()).and_then(
|mut v| {
v.focus_view(s)
},
)
}
s => self.view
.try_borrow_mut()
.map_err(|_| ())
.and_then(|mut v| v.focus_view(s)),
}
}
}

View File

@ -3,9 +3,9 @@ use With;
use XY;
use direction;
use event::{Event, EventResult, Key};
use std::any::Any;
use std::cmp::min;
use std::ops::Deref;
use vec::Vec2;
use view::{Selector, SizeCache};
use view::View;
@ -37,6 +37,25 @@ impl Child {
}
}
struct ChildIterator<I> {
inner: I,
offset: usize,
orientation: direction::Orientation,
}
impl<'a, T: Deref<Target = Child>, I: Iterator<Item = T>> Iterator
for ChildIterator<I> {
type Item = (usize, T);
fn next(&mut self) -> Option<Self::Item> {
self.inner.next().map(|child| {
let previous = self.offset;
self.offset += *child.size.get(self.orientation);
(previous, child)
})
}
}
fn cap<'a, I: Iterator<Item = &'a mut usize>>(iter: I, max: usize) {
let mut available = max;
for item in iter {
@ -108,8 +127,9 @@ impl LinearLayout {
Some(ref cache) => {
// Is our cache even valid?
// Also, is any child invalidating the layout?
if cache.zip_map(req, SizeCache::accept).both() &&
self.children_are_sleeping() {
if cache.zip_map(req, SizeCache::accept).both()
&& self.children_are_sleeping()
{
Some(cache.map(|s| s.value))
} else {
None
@ -126,10 +146,9 @@ impl LinearLayout {
}
/// Returns a cyclic mutable iterator starting with the child in focus
fn iter_mut<'a>(&'a mut self, from_focus: bool,
source: direction::Relative)
-> Box<Iterator<Item = (usize, &mut Child)> + 'a> {
fn iter_mut<'a>(
&'a mut self, from_focus: bool, source: direction::Relative
) -> Box<Iterator<Item = (usize, &mut Child)> + 'a> {
match source {
direction::Relative::Front => {
let start = if from_focus { self.focus } else { 0 };
@ -148,9 +167,8 @@ impl LinearLayout {
}
fn move_focus(&mut self, source: direction::Direction) -> EventResult {
let i = if let Some(i) = source.relative(self.orientation)
.and_then(|rel| {
let i = if let Some(i) =
source.relative(self.orientation).and_then(|rel| {
// The iterator starts at the focused element.
// We don't want that one.
self.iter_mut(true, rel)
@ -165,10 +183,48 @@ impl LinearLayout {
self.focus = i;
EventResult::Consumed(None)
}
fn check_focus_grab(&mut self, event: &Event) {
if let Event::Mouse {
offset,
position,
event,
} = *event
{
if !event.grabs_focus() {
return;
}
let position = match position.checked_sub(offset) {
None => return,
Some(pos) => pos,
};
// Find the selected child
let position = *position.get(self.orientation);
let iterator = ChildIterator {
inner: self.children.iter_mut(),
offset: 0,
orientation: self.orientation,
};
for (i, (offset, child)) in iterator.enumerate() {
let child_size = child.size.get(self.orientation);
// eprintln!("Offset {:?}, size {:?}, position: {:?}", offset, child_size, position);
if (offset + child_size > position)
&& child.view.take_focus(direction::Direction::none())
{
// eprintln!("It's a match!");
self.focus = i;
return;
}
}
}
}
}
fn try_focus((i, child): (usize, &mut Child), source: direction::Direction)
-> Option<usize> {
fn try_focus(
(i, child): (usize, &mut Child), source: direction::Direction
) -> Option<usize> {
if child.view.take_focus(source) {
Some(i)
} else {
@ -187,8 +243,8 @@ impl View for LinearLayout {
// On the axis given by the orientation,
// add the child size to the offset.
*self.orientation.get_ref(&mut offset) += self.orientation
.get(&child.size);
*self.orientation.get_ref(&mut offset) +=
self.orientation.get(&child.size);
}
}
@ -257,10 +313,12 @@ impl View for LinearLayout {
if !desperate.fits_in(req) {
// Just give up...
// TODO: hard-cut
cap(self.children
cap(
self.children
.iter_mut()
.map(|c| c.size.get_mut(orientation)),
*req.get(self.orientation));
*req.get(self.orientation),
);
// TODO: print some error message or something
debug!("Seriously? {:?} > {:?}???", desperate, req);
@ -276,7 +334,8 @@ impl View for LinearLayout {
// Here, we have to make a compromise between the ideal
// and the desperate solutions.
let mut overweight: Vec<(usize, usize)> = sizes.iter()
let mut overweight: Vec<(usize, usize)> = sizes
.iter()
.map(|v| self.orientation.get(v))
.zip(min_sizes.iter().map(|v| self.orientation.get(v)))
.map(|(a, b)| a.saturating_sub(b))
@ -305,7 +364,8 @@ impl View for LinearLayout {
debug!("Allocations: {:?}", allocations);
// Final lengths are the minimum ones + generous allocations
let final_lengths: Vec<Vec2> = min_sizes.iter()
let final_lengths: Vec<Vec2> = min_sizes
.iter()
.map(|v| self.orientation.get(v))
.zip(allocations.iter())
.map(|(a, b)| a + b)
@ -335,11 +395,12 @@ impl View for LinearLayout {
// In what order will we iterate on the children?
let rel = source.relative(self.orientation);
// We activate from_focus only if coming from the "sides".
let i = if let Some(i) = self.iter_mut(rel.is_none(),
rel.unwrap_or(direction::Relative::Front))
.filter_map(|p| try_focus(p, source))
.next() {
let i = if let Some(i) = self.iter_mut(
rel.is_none(),
rel.unwrap_or(direction::Relative::Front),
).filter_map(|p| try_focus(p, source))
.next()
{
// ... we can't update `self.focus` here,
// because rustc thinks we still borrow `self`.
// :(
@ -353,52 +414,66 @@ impl View for LinearLayout {
}
fn on_event(&mut self, event: Event) -> EventResult {
match self.children[self.focus].view.on_event(event.clone()) {
EventResult::Ignored => {
match event {
Event::Shift(Key::Tab) if self.focus > 0 => {
self.move_focus(direction::Direction::back())
}
Event::Key(Key::Tab) if self.focus + 1 <
self.children.len() => {
self.move_focus(direction::Direction::front())
}
Event::Key(Key::Left)
if self.orientation ==
direction::Orientation::Horizontal &&
self.focus > 0 => {
self.move_focus(direction::Direction::right())
}
Event::Key(Key::Up) if self.orientation ==
direction::Orientation::Vertical &&
self.focus > 0 => {
self.move_focus(direction::Direction::down())
}
Event::Key(Key::Right)
if self.orientation ==
direction::Orientation::Horizontal &&
self.focus + 1 <
self.children.len() => {
self.move_focus(direction::Direction::left())
}
Event::Key(Key::Down)
if self.orientation ==
direction::Orientation::Vertical &&
self.focus + 1 <
self.children.len() => {
self.move_focus(direction::Direction::up())
}
_ => EventResult::Ignored,
self.check_focus_grab(&event);
let result = {
let mut iterator = ChildIterator {
inner: self.children.iter_mut(),
offset: 0,
orientation: self.orientation,
};
let (offset, child) = iterator.nth(self.focus).unwrap();
let offset = self.orientation.make_vec(offset, 0);
child.view.on_event(event.relativized(offset))
};
match result {
EventResult::Ignored => match event {
Event::Shift(Key::Tab) if self.focus > 0 => {
self.move_focus(direction::Direction::back())
}
}
Event::Key(Key::Tab)
if self.focus + 1 < self.children.len() =>
{
self.move_focus(direction::Direction::front())
}
Event::Key(Key::Left)
if self.orientation == direction::Orientation::Horizontal
&& self.focus > 0 =>
{
self.move_focus(direction::Direction::right())
}
Event::Key(Key::Up)
if self.orientation == direction::Orientation::Vertical
&& self.focus > 0 =>
{
self.move_focus(direction::Direction::down())
}
Event::Key(Key::Right)
if self.orientation == direction::Orientation::Horizontal
&& self.focus + 1 < self.children.len() =>
{
self.move_focus(direction::Direction::left())
}
Event::Key(Key::Down)
if self.orientation == direction::Orientation::Vertical
&& self.focus + 1 < self.children.len() =>
{
self.move_focus(direction::Direction::up())
}
_ => EventResult::Ignored,
},
res => res,
}
}
fn call_on_any<'a>(&mut self, selector: &Selector,
mut callback: Box<FnMut(&mut Any) + 'a>) {
fn call_on_any<'a>(
&mut self, selector: &Selector,
mut callback: Box<FnMut(&mut Any) + 'a>,
) {
for child in &mut self.children {
child.view.call_on_any(selector, Box::new(|any| callback(any)));
child
.view
.call_on_any(selector, Box::new(|any| callback(any)));
}
}

View File

@ -3,10 +3,8 @@ use Printer;
use With;
use direction;
use event::{Callback, Event, EventResult, Key};
use std::any::Any;
use std::rc::Rc;
use unicode_width::UnicodeWidthStr;
use vec::Vec2;
use view::ScrollBase;
@ -96,7 +94,8 @@ impl ListView {
/// Adds a view to the end of the list.
pub fn add_child<V: View + 'static>(&mut self, label: &str, mut view: V) {
view.take_focus(direction::Direction::none());
self.children.push(ListChild::Row(label.to_string(), Box::new(view)));
self.children
.push(ListChild::Row(label.to_string(), Box::new(view)));
}
/// Removes all children from this view.
@ -126,7 +125,8 @@ impl ListView {
/// Sets a callback to be used when an item is selected.
pub fn set_on_select<F>(&mut self, cb: F)
where F: Fn(&mut Cursive, &String) + 'static
where
F: Fn(&mut Cursive, &String) + 'static,
{
self.on_select = Some(Rc::new(cb));
}
@ -135,7 +135,8 @@ impl ListView {
///
/// Chainable variant.
pub fn on_select<F>(self, cb: F) -> Self
where F: Fn(&mut Cursive, &String) + 'static
where
F: Fn(&mut Cursive, &String) + 'static,
{
self.with(|s| s.set_on_select(cb))
}
@ -147,18 +148,14 @@ impl ListView {
self.focus
}
fn iter_mut<'a>(&'a mut self, from_focus: bool,
source: direction::Relative)
-> Box<Iterator<Item = (usize, &mut ListChild)> + 'a> {
fn iter_mut<'a>(
&'a mut self, from_focus: bool, source: direction::Relative
) -> Box<Iterator<Item = (usize, &mut ListChild)> + 'a> {
match source {
direction::Relative::Front => {
let start = if from_focus { self.focus } else { 0 };
Box::new(self.children
.iter_mut()
.enumerate()
.skip(start))
Box::new(self.children.iter_mut().enumerate().skip(start))
}
direction::Relative::Back => {
let end = if from_focus {
@ -171,19 +168,20 @@ impl ListView {
}
}
fn move_focus(&mut self, n: usize, source: direction::Direction)
-> EventResult {
let i = if let Some(i) =
source.relative(direction::Orientation::Vertical)
.and_then(|rel| {
// The iterator starts at the focused element.
// We don't want that one.
self.iter_mut(true, rel)
.skip(1)
.filter_map(|p| try_focus(p, source))
.take(n)
.last()
}) {
fn move_focus(
&mut self, n: usize, source: direction::Direction
) -> EventResult {
let i = if let Some(i) = source
.relative(direction::Orientation::Vertical)
.and_then(|rel| {
// The iterator starts at the focused element.
// We don't want that one.
self.iter_mut(true, rel)
.skip(1)
.filter_map(|p| try_focus(p, source))
.take(n)
.last()
}) {
i
} else {
return EventResult::Ignored;
@ -197,21 +195,59 @@ impl ListView {
Callback::from_fn(move |s| cb(s, &focused_string))
}))
}
}
fn try_focus((i, child): (usize, &mut ListChild),
source: direction::Direction)
-> Option<usize> {
match *child {
ListChild::Delimiter => None,
ListChild::Row(_, ref mut view) => {
if view.take_focus(source) {
Some(i)
} else {
None
fn labels_width(&self) -> usize {
self.children
.iter()
.map(ListChild::label)
.map(UnicodeWidthStr::width)
.max()
.unwrap_or(0)
}
fn check_focus_grab(&mut self, event: &Event) {
if let Event::Mouse {
offset,
position,
event,
} = *event
{
if !event.grabs_focus() {
return;
}
let position = match position.checked_sub(offset) {
None => return,
Some(pos) => pos,
};
if position.y > self.scrollbase.view_height {
return;
}
// eprintln!("Rel pos: {:?}", position);
// Now that we have a relative position, checks for buttons?
let focus = position.y + self.scrollbase.start_line;
if let ListChild::Row(_, ref mut view) = self.children[focus] {
if view.take_focus(direction::Direction::none()) {
self.focus = focus;
}
}
}
}
}
fn try_focus(
(i, child): (usize, &mut ListChild), source: direction::Direction
) -> Option<usize> {
match *child {
ListChild::Delimiter => None,
ListChild::Row(_, ref mut view) => if view.take_focus(source) {
Some(i)
} else {
None
},
}
}
@ -221,21 +257,17 @@ impl View for ListView {
return;
}
let offset = self.children
.iter()
.map(ListChild::label)
.map(UnicodeWidthStr::width)
.max()
.unwrap_or(0) + 1;
let offset = self.labels_width() + 1;
debug!("Offset: {}", offset);
self.scrollbase.draw(printer, |printer, i| match self.children[i] {
ListChild::Row(ref label, ref view) => {
printer.print((0, 0), label);
view.draw(&printer.offset((offset, 0), i == self.focus));
}
ListChild::Delimiter => (),
});
self.scrollbase
.draw(printer, |printer, i| match self.children[i] {
ListChild::Row(ref label, ref view) => {
printer.print((0, 0), label);
view.draw(&printer.offset((offset, 0), i == self.focus));
}
ListChild::Delimiter => (),
});
}
fn required_size(&mut self, req: Vec2) -> Vec2 {
@ -275,8 +307,8 @@ impl View for ListView {
let spacing = 1;
let scrollbar_width = if self.children.len() > size.y { 2 } else { 0 };
let available = size.x.saturating_sub(label_width + spacing +
scrollbar_width);
let available = size.x
.saturating_sub(label_width + spacing + scrollbar_width);
debug!("Available: {}", available);
@ -290,13 +322,21 @@ impl View for ListView {
return EventResult::Ignored;
}
// First: some events can move the focus around.
self.check_focus_grab(&event);
// Send the event to the focused child.
let labels_width = self.labels_width();
if let ListChild::Row(_, ref mut view) = self.children[self.focus] {
let result = view.on_event(event.clone());
let y = self.focus - self.scrollbase.start_line;
let offset = (labels_width + 1, y);
let result = view.on_event(event.relativized(offset));
if result.is_consumed() {
return result;
}
}
// If the child ignored this event, change the focus.
match event {
Event::Key(Key::Up) if self.focus > 0 => {
self.move_focus(1, direction::Direction::down())
@ -310,15 +350,17 @@ impl View for ListView {
Event::Key(Key::PageDown) => {
self.move_focus(10, direction::Direction::up())
}
Event::Key(Key::Home) |
Event::Ctrl(Key::Home) => {
self.move_focus(usize::max_value(),
direction::Direction::back())
Event::Key(Key::Home) | Event::Ctrl(Key::Home) => {
self.move_focus(
usize::max_value(),
direction::Direction::back(),
)
}
Event::Key(Key::End) |
Event::Ctrl(Key::End) => {
self.move_focus(usize::max_value(),
direction::Direction::front())
Event::Key(Key::End) | Event::Ctrl(Key::End) => {
self.move_focus(
usize::max_value(),
direction::Direction::front(),
)
}
Event::Key(Key::Tab) => {
self.move_focus(1, direction::Direction::front())
@ -332,11 +374,12 @@ impl View for ListView {
fn take_focus(&mut self, source: direction::Direction) -> bool {
let rel = source.relative(direction::Orientation::Vertical);
let i = if let Some(i) =
self.iter_mut(rel.is_none(),
rel.unwrap_or(direction::Relative::Front))
.filter_map(|p| try_focus(p, source))
.next() {
let i = if let Some(i) = self.iter_mut(
rel.is_none(),
rel.unwrap_or(direction::Relative::Front),
).filter_map(|p| try_focus(p, source))
.next()
{
i
} else {
// No one wants to be in focus
@ -347,8 +390,10 @@ impl View for ListView {
true
}
fn call_on_any<'a>(&mut self, selector: &Selector,
mut callback: Box<FnMut(&mut Any) + 'a>) {
fn call_on_any<'a>(
&mut self, selector: &Selector,
mut callback: Box<FnMut(&mut Any) + 'a>,
) {
for view in self.children.iter_mut().filter_map(ListChild::view) {
view.call_on_any(selector, Box::new(|any| callback(any)));
}
@ -356,13 +401,12 @@ impl View for ListView {
fn focus_view(&mut self, selector: &Selector) -> Result<(), ()> {
if let Some(i) = self.children
.iter_mut()
.enumerate()
.filter_map(|(i, v)| v.view().map(|v| (i, v)))
.filter_map(|(i, v)| {
v.focus_view(selector).ok().map(|_| i)
})
.next() {
.iter_mut()
.enumerate()
.filter_map(|(i, v)| v.view().map(|v| (i, v)))
.filter_map(|(i, v)| v.focus_view(selector).ok().map(|_| i))
.next()
{
self.focus = i;
Ok(())
} else {

View File

@ -4,11 +4,10 @@ use Cursive;
use Printer;
use With;
use align::Align;
use event::{Callback, Event, EventResult, Key};
use event::{Callback, Event, EventResult, Key, MouseButton, MouseEvent};
use menu::{MenuItem, MenuTree};
use std::cmp::min;
use std::rc::Rc;
use unicode_width::UnicodeWidthStr;
use vec::Vec2;
use view::{Position, ScrollBase, View};
@ -22,6 +21,7 @@ pub struct MenuPopup {
align: Align,
on_dismiss: Option<Callback>,
on_action: Option<Callback>,
last_size: Vec2,
}
impl MenuPopup {
@ -34,6 +34,7 @@ impl MenuPopup {
align: Align::top_left(),
on_dismiss: None,
on_action: None,
last_size: Vec2::zero(),
}
}
@ -138,32 +139,64 @@ impl MenuPopup {
}
fn make_subtree_cb(&self, tree: &Rc<MenuTree>) -> EventResult {
let tree = tree.clone();
let max_width = 4 +
self.menu
.children
.iter()
.map(Self::item_width)
.max()
.unwrap_or(1);
let tree = Rc::clone(tree);
let max_width = 4
+ self.menu
.children
.iter()
.map(Self::item_width)
.max()
.unwrap_or(1);
let offset = Vec2::new(max_width, self.focus);
let action_cb = self.on_action.clone();
EventResult::with_cb(move |s| {
let action_cb = action_cb.clone();
s.screen_mut()
.add_layer_at(Position::parent(offset),
OnEventView::new(MenuPopup::new(tree.clone())
.on_action(move |s| {
// This will happen when the subtree popup
// activates something;
// First, remove ourselve.
s.pop_layer();
if let Some(ref action_cb) = action_cb {
action_cb.clone()(s);
}
}))
.on_event(Key::Left, |s| s.pop_layer()));
s.screen_mut().add_layer_at(
Position::parent(offset),
OnEventView::new(
MenuPopup::new(Rc::clone(&tree)).on_action(move |s| {
// This will happen when the subtree popup
// activates something;
// First, remove ourselve.
s.pop_layer();
if let Some(ref action_cb) = action_cb {
action_cb.clone()(s);
}
}),
).on_event(Key::Left, |s| s.pop_layer()),
);
})
}
fn submit(&mut self) -> EventResult {
match self.menu.children[self.focus] {
MenuItem::Leaf(_, ref cb) => {
let cb = cb.clone();
let action_cb = self.on_action.clone();
EventResult::with_cb(move |s| {
// Remove ourselves from the face of the earth
s.pop_layer();
// If we had prior orders, do it now.
if let Some(ref action_cb) = action_cb {
action_cb.clone()(s);
}
// And transmit his last words.
cb.clone()(s);
})
}
MenuItem::Subtree(_, ref tree) => self.make_subtree_cb(tree),
_ => panic!("No delimiter here"),
}
}
fn dismiss(&mut self) -> EventResult {
let dismiss_cb = self.on_dismiss.clone();
EventResult::with_cb(move |s| {
if let Some(ref cb) = dismiss_cb {
cb.clone()(s);
}
s.pop_layer();
})
}
}
@ -175,9 +208,9 @@ impl View for MenuPopup {
}
let h = self.menu.len();
// If we're too high, add a vertical offset
let offset = self.align.v.get_offset(h, printer.size.y);
let printer =
&printer.sub_printer(Vec2::new(0, offset), printer.size, true);
let printer = &printer.offset((0, offset), true);
// Start with a box
printer.print_box(Vec2::new(0, 0), printer.size, false);
@ -186,7 +219,8 @@ impl View for MenuPopup {
// But we're keeping the full width,
// to integrate horizontal delimiters in the frame.
let size = printer.size - (0, 2);
let printer = printer.sub_printer(Vec2::new(0, 1), size, true);
let printer = printer.sub_printer((0, 1), size, true);
self.scrollbase.draw(&printer, |printer, i| {
printer.with_selection(i == self.focus, |printer| {
let item = &self.menu.children[i];
@ -211,20 +245,19 @@ impl View for MenuPopup {
printer.print((2, 0), label);
}
}
});
});
}
fn required_size(&mut self, req: Vec2) -> Vec2 {
// We can't really shrink our items here, so it's not flexible.
let w = 4 +
self.menu
.children
.iter()
.map(Self::item_width)
.max()
.unwrap_or(1);
let w = 4
+ self.menu
.children
.iter()
.map(Self::item_width)
.max()
.unwrap_or(1);
let h = 2 + self.menu.children.len();
@ -236,16 +269,8 @@ impl View for MenuPopup {
}
fn on_event(&mut self, event: Event) -> EventResult {
let mut fix_scroll = true;
match event {
Event::Key(Key::Esc) => {
let dismiss_cb = self.on_dismiss.clone();
return EventResult::with_cb(move |s| {
if let Some(ref cb) = dismiss_cb {
cb.clone()(s);
}
s.pop_layer();
});
}
Event::Key(Key::Up) => self.scroll_up(1, true),
Event::Key(Key::PageUp) => self.scroll_up(5, false),
Event::Key(Key::Down) => self.scroll_down(1, true),
@ -253,57 +278,126 @@ impl View for MenuPopup {
Event::Key(Key::Home) => self.focus = 0,
Event::Key(Key::End) => {
self.focus = self.menu
.children
.len()
.saturating_sub(1)
self.focus = self.menu.children.len().saturating_sub(1)
}
Event::Key(Key::Right) if self.menu.children[self.focus]
.is_subtree() => {
Event::Key(Key::Right)
if self.menu.children[self.focus].is_subtree() =>
{
return match self.menu.children[self.focus] {
MenuItem::Subtree(_, ref tree) => {
self.make_subtree_cb(tree)
}
_ => panic!("Not a subtree???"),
};
MenuItem::Subtree(_, ref tree) => {
self.make_subtree_cb(tree)
}
_ => panic!("Not a subtree???"),
};
}
Event::Key(Key::Enter) if !self.menu.children[self.focus]
.is_delimiter() => {
return match self.menu.children[self.focus] {
MenuItem::Leaf(_, ref cb) => {
let cb = cb.clone();
let action_cb = self.on_action.clone();
EventResult::with_cb(move |s| {
// Remove ourselves from the face of the earth
s.pop_layer();
// If we had prior orders, do it now.
if let Some(ref action_cb) = action_cb {
action_cb.clone()(s);
}
// And transmit his last words.
cb.clone()(s);
Event::Key(Key::Enter)
if !self.menu.children[self.focus].is_delimiter() =>
{
return self.submit();
}
Event::Mouse {
event: MouseEvent::WheelUp,
..
} if self.scrollbase.can_scroll_up() =>
{
fix_scroll = false;
self.scrollbase.scroll_up(1);
}
Event::Mouse {
event: MouseEvent::WheelDown,
..
} if self.scrollbase.can_scroll_down() =>
{
fix_scroll = false;
self.scrollbase.scroll_down(1);
}
Event::Mouse {
event: MouseEvent::Press(MouseButton::Left),
position,
offset,
} if self.scrollbase.scrollable()
&& position
.checked_sub(offset + (0, 1))
.map(|position| {
self.scrollbase.start_drag(position, self.last_size.x)
})
.unwrap_or(false) =>
{
fix_scroll = false;
}
Event::Mouse {
event: MouseEvent::Hold(MouseButton::Left),
position,
offset,
} => {
// If the mouse is dragged, we always consume the event.
fix_scroll = false;
position
.checked_sub(offset + (0, 1))
.map(|position| self.scrollbase.drag(position));
}
Event::Mouse {
event: MouseEvent::Press(_),
position,
offset,
} if position.fits_in_rect(offset, self.last_size) =>
{
// eprintln!("Position: {:?} / {:?}", position, offset);
// eprintln!("Last size: {:?}", self.last_size);
let inner_size = self.last_size.saturating_sub((2, 2));
position.checked_sub(offset + (1, 1)).map(
// `position` is not relative to the content
// (It's inside the border)
|position| if position < inner_size {
let focus = position.y + self.scrollbase.start_line;
if !self.menu.children[focus].is_delimiter() {
self.focus = focus;
}
},
);
}
Event::Mouse {
event: MouseEvent::Release(MouseButton::Left),
position,
offset,
} => {
fix_scroll = false;
self.scrollbase.release_grab();
if !self.menu.children[self.focus].is_delimiter() {
if let Some(position) =
position.checked_sub(offset + (1, 1))
{
if position < self.last_size.saturating_sub((2, 2))
&& (position.y + self.scrollbase.start_line
== self.focus)
{
return self.submit();
}
}
}
MenuItem::Subtree(_, ref tree) => {
self.make_subtree_cb(tree)
}
_ => panic!("No delimiter here"),
};
}
Event::Key(Key::Esc) |
Event::Mouse {
event: MouseEvent::Press(_),
..
} => {
return self.dismiss();
}
_ => return EventResult::Ignored,
}
self.scrollbase.scroll_to(self.focus);
if fix_scroll {
self.scrollbase.scroll_to(self.focus);
}
EventResult::Consumed(None)
}
fn layout(&mut self, size: Vec2) {
self.scrollbase.set_heights(size.y.saturating_sub(2),
self.menu.children.len());
self.last_size = size;
self.scrollbase
.set_heights(size.y.saturating_sub(2), self.menu.children.len());
}
}

View File

@ -3,14 +3,12 @@ use Printer;
use direction;
use event::*;
use menu::MenuTree;
use std::rc::Rc;
use theme::ColorStyle;
use unicode_width::UnicodeWidthStr;
use vec::Vec2;
use view::{Position, View};
use views::{OnEventView, MenuPopup};
use views::{MenuPopup, OnEventView};
/// Current state of the menubar
#[derive(PartialEq, Debug)]
@ -36,6 +34,7 @@ enum State {
pub struct Menubar {
/// Menu items in this menubar.
menus: Vec<(String, Rc<MenuTree>)>,
/// TODO: move this out of this view.
pub autohide: bool,
focus: usize,
@ -82,8 +81,9 @@ impl Menubar {
}
/// Insert a new item at the given position.
pub fn insert_subtree(&mut self, i: usize, title: &str, menu: MenuTree)
-> &mut Self {
pub fn insert_subtree(
&mut self, i: usize, title: &str, menu: MenuTree
) -> &mut Self {
self.menus.insert(i, (title.to_string(), Rc::new(menu)));
self
}
@ -108,7 +108,9 @@ impl Menubar {
///
/// Returns `None` if `i > self.len()`
pub fn get_subtree(&mut self, i: usize) -> Option<&mut MenuTree> {
self.menus.get_mut(i).map(|&mut (_, ref mut tree)| Rc::make_mut(tree))
self.menus
.get_mut(i)
.map(|&mut (_, ref mut tree)| Rc::make_mut(tree))
}
/// Looks for an item with the given label.
@ -134,6 +136,39 @@ impl Menubar {
pub fn remove(&mut self, i: usize) {
self.menus.remove(i);
}
fn child_at(&self, x: usize) -> Option<usize> {
if x == 0 {
return None;
}
let mut offset = 1;
for (i, &(ref title, _)) in self.menus.iter().enumerate() {
offset += title.width() + 2;
if x < offset {
return Some(i);
}
}
None
}
fn select_child(&mut self) -> EventResult {
// First, we need a new Rc to send the callback,
// since we don't know when it will be called.
let menu = Rc::clone(&self.menus[self.focus].1);
self.state = State::Submenu;
let offset = (
self.menus[..self.focus]
.iter()
.map(|&(ref title, _)| title.width() + 2)
.fold(0, |a, b| a + b),
if self.autohide { 1 } else { 0 },
);
// Since the closure will be called multiple times,
// we also need a new Rc on every call.
EventResult::with_cb(move |s| show_child(s, offset, Rc::clone(&menu)))
}
}
fn show_child(s: &mut Cursive, offset: (usize, usize), menu: Rc<MenuTree>) {
@ -143,37 +178,35 @@ fn show_child(s: &mut Cursive, offset: (usize, usize), menu: Rc<MenuTree>) {
// (If the view itself listens for a `left` or `right` press, it will
// consume it before our OnEventView. This means sub-menus can properly
// be entered.)
s.screen_mut()
.add_layer_at(Position::absolute(offset),
OnEventView::new(MenuPopup::new(menu)
.on_dismiss(|s| {
s.select_menubar()
})
.on_action(|s| {
s.menubar().state =
State::Inactive
}))
.on_event(Key::Right, |s| {
s.screen_mut().add_layer_at(
Position::absolute(offset),
OnEventView::new(
MenuPopup::new(menu)
.on_dismiss(|s| s.select_menubar())
.on_action(|s| s.menubar().state = State::Inactive),
).on_event(Key::Right, |s| {
s.pop_layer();
s.select_menubar();
// Act as if we sent "Right" then "Down"
s.menubar().on_event(Event::Key(Key::Right)).process(s);
if let EventResult::Consumed(Some(cb)) =
s.menubar().on_event(Event::Key(Key::Down)) {
s.menubar().on_event(Event::Key(Key::Down))
{
cb(s);
}
})
.on_event(Key::Left, |s| {
s.pop_layer();
s.select_menubar();
// Act as if we sent "Left" then "Down"
s.menubar().on_event(Event::Key(Key::Left)).process(s);
if let EventResult::Consumed(Some(cb)) =
s.menubar().on_event(Event::Key(Key::Down)) {
cb(s);
}
}));
.on_event(Key::Left, |s| {
s.pop_layer();
s.select_menubar();
// Act as if we sent "Left" then "Down"
s.menubar().on_event(Event::Key(Key::Left)).process(s);
if let EventResult::Consumed(Some(cb)) =
s.menubar().on_event(Event::Key(Key::Down))
{
cb(s);
}
}),
);
}
impl View for Menubar {
@ -188,54 +221,68 @@ impl View for Menubar {
for (i, &(ref title, _)) in self.menus.iter().enumerate() {
// We don't want to show HighlightInactive when we're not selected,
// because it's ugly on the menubar.
let selected = (self.state != State::Inactive) &&
(i == self.focus);
let selected =
(self.state != State::Inactive) && (i == self.focus);
printer.with_selection(selected, |printer| {
printer.print((offset, 0), &format!(" {} ", title));
offset += title.width() + 2;
});
offset += title.width() + 2;
}
}
fn on_event(&mut self, event: Event) -> EventResult {
match event {
Event::Key(Key::Esc) => {
Event::Key(Key::Esc) if self.autohide => {
self.hide();
return EventResult::with_cb(|s| s.clear());
}
Event::Key(Key::Left) => {
if self.focus > 0 {
self.focus -= 1
} else {
self.focus = self.menus.len() - 1
}
Event::Key(Key::Left) => if self.focus > 0 {
self.focus -= 1
} else {
self.focus = self.menus.len() - 1
},
Event::Key(Key::Right) => if self.focus + 1 < self.menus.len() {
self.focus += 1
} else {
self.focus = 0
},
Event::Key(Key::Down) | Event::Key(Key::Enter) => {
return self.select_child();
}
Event::Key(Key::Right) => {
if self.focus + 1 < self.menus.len() {
self.focus += 1
} else {
self.focus = 0
}
Event::Mouse {
event: MouseEvent::Press(_),
position,
offset,
} if position.fits(offset) && position.y == offset.y =>
{
position
.checked_sub(offset)
.and_then(|pos| self.child_at(pos.x))
.map(|child| {
self.focus = child;
});
}
Event::Key(Key::Down) |
Event::Key(Key::Enter) => {
// First, we need a new Rc to send the callback,
// since we don't know when it will be called.
let menu = self.menus[self.focus].1.clone();
self.state = State::Submenu;
let offset =
(self.menus[..self.focus]
.iter()
.map(|&(ref title, _)| title.width() + 2)
.fold(0, |a, b| a + b),
if self.autohide { 1 } else { 0 });
// Since the closure will be called multiple times,
// we also need a new Rc on every call.
return EventResult::with_cb(move |s| {
show_child(s,
offset,
menu.clone())
});
Event::Mouse {
event: MouseEvent::Press(_),
..
} => {
self.hide();
return EventResult::with_cb(|s| s.clear());
}
Event::Mouse {
event: MouseEvent::Release(MouseButton::Left),
position,
offset,
} if position.fits(offset) && position.y == offset.y =>
{
if let Some(child) = position
.checked_sub(offset)
.and_then(|pos| self.child_at(pos.x))
{
if self.focus == child {
return self.select_child();
}
}
}
_ => return EventResult::Ignored,
}

View File

@ -69,15 +69,15 @@ pub use self::dialog::Dialog;
pub use self::dummy::DummyView;
pub use self::edit_view::EditView;
pub use self::id_view::{IdView, ViewRef};
pub use self::on_event_view::OnEventView;
pub use self::layer::Layer;
pub use self::linear_layout::LinearLayout;
pub use self::list_view::{ListChild, ListView};
pub use self::menu_popup::MenuPopup;
pub use self::menubar::Menubar;
pub use self::on_event_view::OnEventView;
pub use self::panel::Panel;
pub use self::progress_bar::{Counter, ProgressBar};
pub use self::radio::{RadioGroup, RadioButton};
pub use self::radio::{RadioButton, RadioGroup};
pub use self::select_view::SelectView;
pub use self::shadow_view::ShadowView;
pub use self::sized_view::SizedView;

View File

@ -2,7 +2,6 @@ use Cursive;
use With;
use event::{Callback, Event, EventResult};
use std::collections::HashMap;
use std::rc::Rc;
use view::{View, ViewWrapper};
@ -35,7 +34,7 @@ impl<T> Clone for Action<T> {
fn clone(&self) -> Self {
Action {
phase: self.phase.clone(),
callback: self.callback.clone(),
callback: Rc::clone(&self.callback),
}
}
}

View File

@ -1,4 +1,5 @@
use Printer;
use event::{Event, EventResult};
use vec::Vec2;
use view::{View, ViewWrapper};
@ -18,6 +19,10 @@ impl<V: View> Panel<V> {
impl<V: View> ViewWrapper for Panel<V> {
wrap_impl!(self.view: V);
fn wrap_on_event(&mut self, event: Event) -> EventResult {
self.view.on_event(event.relativized((1, 1)))
}
fn wrap_required_size(&mut self, req: Vec2) -> Vec2 {
// TODO: make borders conditional?
let req = req.saturating_sub((2, 2));
@ -27,9 +32,11 @@ impl<V: View> ViewWrapper for Panel<V> {
fn wrap_draw(&self, printer: &Printer) {
printer.print_box((0, 0), printer.size, true);
self.view.draw(&printer.sub_printer((1, 1),
printer.size.saturating_sub((2, 2)),
true));
self.view.draw(&printer.sub_printer(
(1, 1),
printer.size.saturating_sub((2, 2)),
true,
));
}
fn wrap_layout(&mut self, size: Vec2) {

View File

@ -5,7 +5,6 @@ use align::HAlign;
use std::cmp;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::thread;
use theme::{ColorStyle, Effect};
use view::View;
@ -118,14 +117,17 @@ impl ProgressBar {
pub fn start<F: FnOnce(Counter) + Send + 'static>(&mut self, f: F) {
let counter: Counter = self.value.clone();
thread::spawn(move || { f(counter); });
thread::spawn(move || {
f(counter);
});
}
/// Starts a function in a separate thread, and monitor the progress.
///
/// Chainable variant.
pub fn with_task<F: FnOnce(Counter) + Send + 'static>(mut self, task: F)
-> Self {
pub fn with_task<F: FnOnce(Counter) + Send + 'static>(
mut self, task: F
) -> Self {
self.start(task);
self
}
@ -143,9 +145,9 @@ impl ProgressBar {
/// format!("{} %", percent)
/// }
/// ```
pub fn with_label<F: Fn(usize, (usize, usize)) -> String + 'static>
(mut self, label_maker: F)
-> Self {
pub fn with_label<F: Fn(usize, (usize, usize)) -> String + 'static>(
mut self, label_maker: F
) -> Self {
self.label_maker = Box::new(label_maker);
self
}

View File

@ -1,7 +1,6 @@
use {Printer, With};
use direction::Direction;
use event::{Event, EventResult, Key};
use event::{Event, EventResult, Key, MouseButton, MouseEvent};
use std::cell::RefCell;
use std::rc::Rc;
use theme::ColorStyle;
@ -15,7 +14,7 @@ struct SharedState<T> {
impl<T> SharedState<T> {
pub fn selection(&self) -> Rc<T> {
self.values[self.selection].clone()
Rc::clone(&self.values[self.selection])
}
}
@ -32,7 +31,7 @@ pub struct RadioGroup<T> {
state: Rc<RefCell<SharedState<T>>>,
}
impl <T> Default for RadioGroup<T> {
impl<T> Default for RadioGroup<T> {
fn default() -> Self {
Self::new()
}
@ -52,11 +51,12 @@ impl<T> RadioGroup<T> {
/// Adds a new button to the group.
///
/// The button will display `label` next to it, and will embed `value`.
pub fn button<S: Into<String>>(&mut self, value: T, label: S)
-> RadioButton<T> {
pub fn button<S: Into<String>>(
&mut self, value: T, label: S
) -> RadioButton<T> {
let count = self.state.borrow().values.len();
self.state.borrow_mut().values.push(Rc::new(value));
RadioButton::new(self.state.clone(), count, label.into())
RadioButton::new(Rc::clone(&self.state), count, label.into())
}
/// Returns the id of the selected button.
@ -74,8 +74,9 @@ impl<T> RadioGroup<T> {
impl RadioGroup<String> {
/// Adds a button, using the label itself as value.
pub fn button_str<S: Into<String>>(&mut self, text: S)
-> RadioButton<String> {
pub fn button_str<S: Into<String>>(
&mut self, text: S
) -> RadioButton<String> {
let text = text.into();
self.button(text.clone(), text)
}
@ -103,8 +104,9 @@ pub struct RadioButton<T> {
impl<T> RadioButton<T> {
impl_enabled!(self.enabled);
fn new(state: Rc<RefCell<SharedState<T>>>, id: usize, label: String)
-> Self {
fn new(
state: Rc<RefCell<SharedState<T>>>, id: usize, label: String
) -> Self {
RadioButton {
state: state,
id: id,
@ -142,16 +144,20 @@ impl<T> RadioButton<T> {
printer.print((4, 0), &self.label);
}
}
}
impl<T> View for RadioButton<T> {
fn required_size(&mut self, _: Vec2) -> Vec2 {
fn req_size(&self) -> Vec2 {
if self.label.is_empty() {
Vec2::new(3, 1)
} else {
Vec2::new(3 + 1 + self.label.len(), 1)
}
}
}
impl<T> View for RadioButton<T> {
fn required_size(&mut self, _: Vec2) -> Vec2 {
self.req_size()
}
fn take_focus(&mut self, _: Direction) -> bool {
self.enabled
@ -159,18 +165,30 @@ impl<T> View for RadioButton<T> {
fn draw(&self, printer: &Printer) {
if self.enabled {
printer.with_selection(printer.focused,
|printer| self.draw_internal(printer));
printer.with_selection(
printer.focused,
|printer| self.draw_internal(printer),
);
} else {
printer.with_color(ColorStyle::Secondary,
|printer| self.draw_internal(printer));
printer.with_color(
ColorStyle::Secondary,
|printer| self.draw_internal(printer),
);
}
}
fn on_event(&mut self, event: Event) -> EventResult {
match event {
Event::Key(Key::Enter) |
Event::Char(' ') => {
Event::Key(Key::Enter) | Event::Char(' ') => {
self.select();
EventResult::Consumed(None)
}
Event::Mouse {
event: MouseEvent::Release(MouseButton::Left),
position,
offset,
} if position.fits_in_rect(offset, self.req_size()) =>
{
self.select();
EventResult::Consumed(None)
}

View File

@ -3,14 +3,13 @@ use Printer;
use With;
use align::{Align, HAlign, VAlign};
use direction::Direction;
use event::{Callback, Event, EventResult, Key};
use event::{Callback, Event, EventResult, Key, MouseButton, MouseEvent};
use menu::MenuTree;
use std::borrow::Borrow;
use std::cell::Cell;
use std::cmp::min;
use std::rc::Rc;
use theme::ColorStyle;
use unicode_width::UnicodeWidthStr;
use vec::Vec2;
use view::{Position, ScrollBase, View};
@ -201,7 +200,7 @@ impl<T: 'static> SelectView<T> {
///
/// Panics if the list is empty.
pub fn selection(&self) -> Rc<T> {
self.items[self.focus()].value.clone()
Rc::clone(&self.items[self.focus()].value)
}
/// Removes all items from this view.
@ -319,6 +318,196 @@ impl<T: 'static> SelectView<T> {
let focus = min(self.focus() + n, self.items.len().saturating_sub(1));
self.focus.set(focus);
}
fn submit(&mut self) -> EventResult {
let cb = self.on_submit.clone().unwrap();
let v = self.selection();
// We return a Callback Rc<|s| cb(s, &*v)>
EventResult::Consumed(Some(Callback::from_fn(move |s| cb(s, &v))))
}
fn on_event_regular(&mut self, event: Event) -> EventResult {
let mut fix_scroll = true;
match event {
Event::Key(Key::Up) if self.focus() > 0 => self.focus_up(1),
Event::Key(Key::Down) if self.focus() + 1 < self.items.len() => {
self.focus_down(1)
}
Event::Key(Key::PageUp) => self.focus_up(10),
Event::Key(Key::PageDown) => self.focus_down(10),
Event::Key(Key::Home) => self.focus.set(0),
Event::Key(Key::End) => {
self.focus.set(self.items.len().saturating_sub(1))
}
Event::Mouse {
event: MouseEvent::WheelDown,
..
} if self.scrollbase.can_scroll_down() =>
{
fix_scroll = false;
self.scrollbase.scroll_down(5);
}
Event::Mouse {
event: MouseEvent::WheelUp,
..
} if self.scrollbase.can_scroll_up() =>
{
fix_scroll = false;
self.scrollbase.scroll_up(5);
}
Event::Mouse {
event: MouseEvent::Press(MouseButton::Left),
position,
offset,
} if position
.checked_sub(offset)
.map(|position| {
self.scrollbase.start_drag(position, self.last_size.x)
})
.unwrap_or(false) =>
{
fix_scroll = false;
}
Event::Mouse {
event: MouseEvent::Hold(MouseButton::Left),
position,
offset,
} => {
// If the mouse is dragged, we always consume the event.
fix_scroll = false;
position
.checked_sub(offset)
.map(|position| self.scrollbase.drag(position));
}
Event::Mouse {
event: MouseEvent::Press(_),
position,
offset,
} => if let Some(position) = position.checked_sub(offset) {
if position < self.last_size {
fix_scroll = false;
self.focus.set(position.y + self.scrollbase.start_line);
}
},
Event::Mouse {
event: MouseEvent::Release(MouseButton::Left),
position,
offset,
} => {
fix_scroll = false;
self.scrollbase.release_grab();
if self.on_submit.is_some() {
if let Some(position) = position.checked_sub(offset) {
if position < self.last_size
&& (position.y + self.scrollbase.start_line)
== self.focus()
{
return self.submit();
}
}
}
}
Event::Key(Key::Enter) if self.on_submit.is_some() => {
return self.submit();
}
Event::Char(c) => {
// Starting from the current focus,
// find the first item that match the char.
// Cycle back to the beginning of
// the list when we reach the end.
// This is achieved by chaining twice the iterator
let iter = self.items.iter().chain(self.items.iter());
if let Some((i, _)) = iter.enumerate()
.skip(self.focus() + 1)
.find(|&(_, item)| item.label.starts_with(c))
{
// Apply modulo in case we have a hit
// from the chained iterator
self.focus.set(i % self.items.len());
}
}
_ => return EventResult::Ignored,
}
if fix_scroll {
let focus = self.focus();
self.scrollbase.scroll_to(focus);
}
EventResult::Consumed(self.on_select.clone().map(|cb| {
let v = self.selection();
Callback::from_fn(move |s| cb(s, &v))
}))
}
fn open_popup(&mut self) -> EventResult {
// 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 = Rc::clone(&self.focus);
let on_submit = self.on_submit.as_ref().cloned();
let value = Rc::clone(&item.value);
tree.add_leaf(item.label.clone(), move |s| {
focus.set(i);
if let Some(ref on_submit) = on_submit {
on_submit(s, &value);
}
});
}
// 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 item_length = self.items[focus].label.len();
let text_offset = (self.last_size.x.saturating_sub(item_length)) / 2;
// The total offset for the window is:
// * the last absolute offset at which we drew this view
// * shifted to the right of the text offset
// * shifted to the top of the focus (so the line matches)
// * shifted top-left of the border+padding of the popup
let offset = self.last_offset.get();
let offset = offset + (text_offset, 0);
let offset = offset.saturating_sub((0, focus));
let offset = offset.saturating_sub((2, 1));
// And now, we can return the callback that will create the popup.
EventResult::with_cb(move |s| {
// The callback will want to work with a fresh Rc
let tree = Rc::clone(&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.signed() - current_offset;
// And finally, put the view in view!
s.screen_mut().add_layer_at(
Position::parent(offset),
MenuPopup::new(tree).focus(focus),
);
})
}
// A popup view only does one thing: open the popup on Enter.
fn on_event_popup(&mut self, event: Event) -> EventResult {
match event {
// TODO: add Left/Right support for quick-switch?
Event::Key(Key::Enter) => self.open_popup(),
Event::Mouse {
event: MouseEvent::Release(MouseButton::Left),
position,
offset,
} if position.fits_in_rect(offset, self.last_size) =>
{
self.open_popup()
}
_ => EventResult::Ignored,
}
}
}
impl SelectView<String> {
@ -440,109 +629,9 @@ impl<T: 'static> View for SelectView<T> {
fn on_event(&mut self, event: Event) -> EventResult {
if self.popup {
match event {
// TODO: add Left/Right support for quick-switch?
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();
let on_submit = self.on_submit.as_ref().cloned();
let value = item.value.clone();
tree.add_leaf(item.label.clone(), move |s| {
focus.set(i);
if let Some(ref on_submit) = on_submit {
on_submit(s, &value);
}
});
}
// 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 item_length = self.items[focus].label.len();
let text_offset =
(self.last_size.x.saturating_sub(item_length)) / 2;
// The total offset for the window is:
// * the last absolute offset at which we drew this view
// * shifted to the right of the text offset
// * shifted to the top of the focus (so the line matches)
// * shifted top-left of the border+padding of the popup
let offset = self.last_offset.get();
let offset = offset + (text_offset, 0);
let offset = offset.saturating_sub((0, focus));
let offset = offset.saturating_sub((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();
// 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.signed() - 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,
}
self.on_event_popup(event)
} else {
match event {
Event::Key(Key::Up) if self.focus() > 0 => self.focus_up(1),
Event::Key(Key::Down)
if self.focus() + 1 < self.items.len() =>
{
self.focus_down(1)
}
Event::Key(Key::PageUp) => self.focus_up(10),
Event::Key(Key::PageDown) => self.focus_down(10),
Event::Key(Key::Home) => self.focus.set(0),
Event::Key(Key::End) => {
self.focus.set(self.items.len().saturating_sub(1))
}
Event::Key(Key::Enter) if self.on_submit.is_some() => {
let cb = self.on_submit.clone().unwrap();
let v = self.selection();
// We return a Callback Rc<|s| cb(s, &*v)>
return EventResult::Consumed(
Some(Callback::from_fn(move |s| cb(s, &v))),
);
}
Event::Char(c) => {
// Starting from the current focus,
// find the first item that match the char.
// Cycle back to the beginning of
// the list when we reach the end.
// This is achieved by chaining twice the iterator
let iter = self.items.iter().chain(self.items.iter());
if let Some((i, _)) = iter.enumerate()
.skip(self.focus() + 1)
.find(|&(_, item)| item.label.starts_with(c))
{
// Apply modulo in case we have a hit
// from the chained iterator
self.focus.set(i % self.items.len());
}
}
_ => return EventResult::Ignored,
}
let focus = self.focus();
self.scrollbase.scroll_to(focus);
EventResult::Consumed(self.on_select.clone().map(|cb| {
let v = self.selection();
Callback::from_fn(move |s| cb(s, &v))
}))
self.on_event_regular(event)
}
}

View File

@ -1,4 +1,5 @@
use Printer;
use event::{Event, EventResult};
use theme::ColorStyle;
use vec::Vec2;
use view::{View, ViewWrapper};
@ -23,8 +24,11 @@ impl<T: View> ShadowView<T> {
}
fn padding(&self) -> Vec2 {
Vec2::new(1 + self.left_padding as usize,
1 + self.top_padding as usize)
self.top_left_padding() + (1, 1)
}
fn top_left_padding(&self) -> Vec2 {
Vec2::new(self.left_padding as usize, self.top_padding as usize)
}
/// If set, adds an empty column to the left of the view.
@ -58,17 +62,22 @@ impl<T: View> ViewWrapper for ShadowView<T> {
self.view.layout(size.saturating_sub(offset));
}
fn wrap_draw(&self, printer: &Printer) {
fn wrap_on_event(&mut self, event: Event) -> EventResult {
let padding = self.top_left_padding();
self.view.on_event(event.relativized(padding))
}
if printer.size.y <= self.top_padding as usize ||
printer.size.x <= self.left_padding as usize {
fn wrap_draw(&self, printer: &Printer) {
if printer.size.y <= self.top_padding as usize
|| printer.size.x <= self.left_padding as usize
{
// Nothing to do if there's no place to draw.
return;
}
// Skip the first row/column
let offset = Vec2::new(self.left_padding as usize,
self.top_padding as usize);
let offset =
Vec2::new(self.left_padding as usize, self.top_padding as usize);
let printer = &printer.offset(offset, true);
if printer.theme.shadow {
let h = printer.size.y;
@ -85,9 +94,11 @@ impl<T: View> ViewWrapper for ShadowView<T> {
}
// Draw the view background
let printer = printer.sub_printer(Vec2::zero(),
printer.size.saturating_sub((1, 1)),
true);
let printer = printer.sub_printer(
Vec2::zero(),
printer.size.saturating_sub((1, 1)),
true,
);
self.view.draw(&printer);
}
}

View File

@ -1,8 +1,7 @@
use {Cursive, Printer};
use With;
use direction::{Direction, Orientation};
use event::{Callback, Event, EventResult, Key};
use event::{Callback, Event, EventResult, Key, MouseButton, MouseEvent};
use std::rc::Rc;
use theme::ColorStyle;
use vec::Vec2;
@ -15,6 +14,7 @@ pub struct SliderView {
on_enter: Option<Rc<Fn(&mut Cursive, usize)>>,
value: usize,
max_value: usize,
dragging: bool,
}
impl SliderView {
@ -29,6 +29,7 @@ impl SliderView {
max_value: max_value,
on_change: None,
on_enter: None,
dragging: false,
}
}
@ -55,12 +56,15 @@ impl SliderView {
///
/// Chainable variant.
pub fn value(self, value: usize) -> Self {
self.with(|s| { s.set_value(value); })
self.with(|s| {
s.set_value(value);
})
}
/// Sets a callback to be called when the slider is moved.
pub fn on_change<F>(mut self, callback: F) -> Self
where F: Fn(&mut Cursive, usize) + 'static
where
F: Fn(&mut Cursive, usize) + 'static,
{
self.on_change = Some(Rc::new(callback));
self
@ -68,7 +72,8 @@ impl SliderView {
/// Sets a callback to be called when the <Enter> key is pressed.
pub fn on_enter<F>(mut self, callback: F) -> Self
where F: Fn(&mut Cursive, usize) + 'static
where
F: Fn(&mut Cursive, usize) + 'static,
{
self.on_enter = Some(Rc::new(callback));
self
@ -77,7 +82,9 @@ impl SliderView {
fn get_change_result(&self) -> EventResult {
EventResult::Consumed(self.on_change.clone().map(|cb| {
let value = self.value;
Callback::from_fn(move |s| { cb(s, value); })
Callback::from_fn(move |s| {
cb(s, value);
})
}))
}
@ -98,6 +105,10 @@ impl SliderView {
EventResult::Ignored
}
}
fn req_size(&self) -> Vec2 {
self.orientation.make_vec(self.max_value, 1)
}
}
impl View for SliderView {
@ -122,29 +133,71 @@ impl View for SliderView {
}
fn required_size(&mut self, _: Vec2) -> Vec2 {
self.orientation.make_vec(self.max_value, 1)
self.req_size()
}
fn on_event(&mut self, event: Event) -> EventResult {
match event {
Event::Key(Key::Left) if self.orientation ==
Orientation::Horizontal => {
Event::Key(Key::Left)
if self.orientation == Orientation::Horizontal =>
{
self.slide_minus()
}
Event::Key(Key::Right) if self.orientation ==
Orientation::Horizontal => {
Event::Key(Key::Right)
if self.orientation == Orientation::Horizontal =>
{
self.slide_plus()
}
Event::Key(Key::Up) if self.orientation ==
Orientation::Vertical => self.slide_minus(),
Event::Key(Key::Down) if self.orientation ==
Orientation::Vertical => {
Event::Key(Key::Up)
if self.orientation == Orientation::Vertical =>
{
self.slide_minus()
}
Event::Key(Key::Down)
if self.orientation == Orientation::Vertical =>
{
self.slide_plus()
}
Event::Key(Key::Enter) if self.on_enter.is_some() => {
let value = self.value;
let cb = self.on_enter.clone().unwrap();
EventResult::with_cb(move |s| { cb(s, value); })
EventResult::with_cb(move |s| {
cb(s, value);
})
}
Event::Mouse {
event: MouseEvent::Hold(MouseButton::Left),
position,
offset,
} if self.dragging =>
{
let position = position.saturating_sub(offset);
let position = self.orientation.get(&position);
let position = ::std::cmp::min(
position,
self.max_value.saturating_sub(1),
);
self.value = position;
self.get_change_result()
}
Event::Mouse {
event: MouseEvent::Press(MouseButton::Left),
position,
offset,
} if position.fits_in_rect(offset, self.req_size()) =>
{
position.checked_sub(offset).map(|position| {
self.dragging = true;
self.value = self.orientation.get(&position);
});
self.get_change_result()
}
Event::Mouse {
event: MouseEvent::Release(MouseButton::Left),
..
} => {
self.dragging = false;
EventResult::Ignored
}
_ => EventResult::Ignored,
}

View File

@ -1,9 +1,9 @@
use Printer;
use ::With;
use With;
use direction::Direction;
use event::{Event, EventResult};
use std::any::Any;
use std::ops::Deref;
use theme::ColorStyle;
use vec::Vec2;
use view::{Offset, Position, Selector, View};
@ -22,11 +22,13 @@ enum Placement {
}
impl Placement {
pub fn compute_offset<S, A, P>(&self, size: S, available: A, parent: P)
-> Vec2
where S: Into<Vec2>,
A: Into<Vec2>,
P: Into<Vec2>
pub fn compute_offset<S, A, P>(
&self, size: S, available: A, parent: P
) -> Vec2
where
S: Into<Vec2>,
A: Into<Vec2>,
P: Into<Vec2>,
{
match *self {
Placement::Floating(ref position) => {
@ -43,8 +45,9 @@ struct Child {
placement: Placement,
// We cannot call `take_focus` until we've called `layout()`
// So we want to call `take_focus` right after the first call
// to `layout`; this flag remembers when we've done that.
// (for instance, a textView must know it will scroll to be focusable).
// So we want to call `take_focus` right after the first call to `layout`.
// This flag remembers when we've done that.
virgin: bool,
}
@ -63,7 +66,8 @@ impl StackView {
///
/// Fullscreen layers have no shadow.
pub fn add_fullscreen_layer<T>(&mut self, view: T)
where T: 'static + View
where
T: 'static + View,
{
self.layers.push(Child {
view: Box::new(Layer::new(view)),
@ -75,7 +79,8 @@ impl StackView {
/// Adds new view on top of the stack in the center of the screen.
pub fn add_layer<T>(&mut self, view: T)
where T: 'static + View
where
T: 'static + View,
{
self.add_layer_at(Position::center(), view);
}
@ -84,7 +89,8 @@ impl StackView {
///
/// Chainable variant.
pub fn layer<T>(self, view: T) -> Self
where T: 'static + View
where
T: 'static + View,
{
self.with(|s| s.add_layer(view))
}
@ -93,20 +99,24 @@ impl StackView {
///
/// Chainable variant.
pub fn fullscreen_layer<T>(self, view: T) -> Self
where T: 'static + View
where
T: 'static + View,
{
self.with(|s| s.add_fullscreen_layer(view))
}
/// Adds a view on top of the stack.
pub fn add_layer_at<T>(&mut self, position: Position, view: T)
where T: 'static + View
where
T: 'static + View,
{
self.layers.push(Child {
// Skip padding for absolute/parent-placed views
view: Box::new(ShadowView::new(Layer::new(view))
.top_padding(position.y == Offset::Center)
.left_padding(position.x == Offset::Center)),
view: Box::new(
ShadowView::new(Layer::new(view))
.top_padding(position.y == Offset::Center)
.left_padding(position.x == Offset::Center),
),
size: Vec2::new(0, 0),
placement: Placement::Floating(position),
virgin: true,
@ -117,7 +127,8 @@ impl StackView {
///
/// Chainable variant.
pub fn layer_at<T>(self, position: Position, view: T) -> Self
where T: 'static + View
where
T: 'static + View,
{
self.with(|s| s.add_layer_at(position, view))
}
@ -131,8 +142,11 @@ impl StackView {
pub fn offset(&self) -> Vec2 {
let mut previous = Vec2::zero();
for layer in &self.layers {
let offset = layer.placement
.compute_offset(layer.size, self.last_size, previous);
let offset = layer.placement.compute_offset(
layer.size,
self.last_size,
previous,
);
previous = offset;
}
previous
@ -144,18 +158,53 @@ impl StackView {
}
}
struct StackPositionIterator<R: Deref<Target = Child>, I: Iterator<Item = R>> {
inner: I,
previous: Vec2,
total_size: Vec2,
}
impl<R: Deref<Target = Child>, I: Iterator<Item = R>>
StackPositionIterator<R, I> {
/// Returns a new StackPositionIterator
pub fn new(inner: I, total_size: Vec2) -> Self {
let previous = Vec2::zero();
StackPositionIterator {
inner,
previous,
total_size,
}
}
}
impl<R: Deref<Target = Child>, I: Iterator<Item = R>> Iterator
for StackPositionIterator<R, I> {
type Item = (R, Vec2);
fn next(&mut self) -> Option<(R, Vec2)> {
self.inner.next().map(|v| {
let offset = v.placement.compute_offset(
v.size,
self.total_size,
self.previous,
);
self.previous = offset;
// eprintln!("{:?}", offset);
(v, offset)
})
}
}
impl View for StackView {
fn draw(&self, printer: &Printer) {
let last = self.layers.len();
let mut previous = Vec2::zero();
printer.with_color(ColorStyle::Primary, |printer| {
for (i, v) in self.layers.iter().enumerate() {
// Place the view
// Center the view
let offset = v.placement
.compute_offset(v.size, printer.size, previous);
previous = offset;
for (i, (v, offset)) in
StackPositionIterator::new(self.layers.iter(), printer.size)
.enumerate()
{
v.view
.draw(&printer.sub_printer(offset, v.size, i + 1 == last));
}
@ -163,9 +212,15 @@ impl View for StackView {
}
fn on_event(&mut self, event: Event) -> EventResult {
match self.layers.last_mut() {
// Use the stack position iterator to get the offset of the top layer.
// TODO: save it instead when drawing?
match StackPositionIterator::new(
self.layers.iter_mut(),
self.last_size,
).last()
{
None => EventResult::Ignored,
Some(v) => v.view.on_event(event),
Some((v, offset)) => v.view.on_event(event.relativized(offset)),
}
}
@ -208,10 +263,14 @@ impl View for StackView {
}
}
fn call_on_any<'a>(&mut self, selector: &Selector,
mut callback: Box<FnMut(&mut Any) + 'a>) {
fn call_on_any<'a>(
&mut self, selector: &Selector,
mut callback: Box<FnMut(&mut Any) + 'a>,
) {
for layer in &mut self.layers {
layer.view.call_on_any(selector, Box::new(|any| callback(any)));
layer
.view
.call_on_any(selector, Box::new(|any| callback(any)));
}
}

View File

@ -2,12 +2,12 @@
use {Printer, With, XY};
use direction::Direction;
use event::{Event, EventResult, Key};
use event::{Event, EventResult, Key, MouseEvent};
use odds::vec::VecExt;
use theme::{ColorStyle, Effect};
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
use utils::{LinesIterator, Row, prefix};
use utils::{prefix, simple_prefix, LinesIterator, Row};
use vec::Vec2;
use view::{ScrollBase, SizeCache, View};
@ -184,9 +184,8 @@ impl TextArea {
// If the current line is full, adding a character will overflow into the next line. To
// show that, we need to add a fake "ghost" row, just for the cursor.
fn fix_ghost_row(&mut self) {
if self.rows.is_empty() ||
self.rows.last().unwrap().end != self.content.len()
if self.rows.is_empty()
|| self.rows.last().unwrap().end != self.content.len()
{
// Add a fake, empty row at the end.
self.rows.push(Row {
@ -267,7 +266,6 @@ impl TextArea {
}
fn insert(&mut self, ch: char) {
// First, we inject the data, but keep the cursor unmoved
// (So the cursor is to the left of the injected char)
self.content.insert(self.cursor, ch);
@ -313,17 +311,14 @@ impl TextArea {
// We don't need to go beyond a newline.
// If we don't find one, end of the text it is.
debug!("Cursor: {}", self.cursor);
let last_byte = self.content[self.cursor..].find('\n').map(|i| {
1 + i + self.cursor
});
let last_row = last_byte.map_or(self.rows.len(), |last_byte| {
self.row_at(last_byte)
});
let last_byte = self.content[self.cursor..]
.find('\n')
.map(|i| 1 + i + self.cursor);
let last_row = last_byte
.map_or(self.rows.len(), |last_byte| self.row_at(last_byte));
let last_byte = last_byte.unwrap_or_else(|| self.content.len());
debug!("Content: `{}` (len={})",
self.content,
self.content.len());
debug!("Content: `{}` (len={})", self.content, self.content.len());
debug!("start/end: {}/{}", first_byte, last_byte);
debug!("start/end rows: {}/{}", first_row, last_row);
@ -343,8 +338,8 @@ impl TextArea {
// How much did this add?
debug!("New rows: {:?}", new_rows);
debug!("{}-{}", first_row, last_row);
let new_row_count = self.rows.len() + new_rows.len() + first_row -
last_row;
let new_row_count =
self.rows.len() + new_rows.len() + first_row - last_row;
if !scrollable && new_row_count > size.y {
// We just changed scrollable status.
// This changes everything.
@ -376,7 +371,8 @@ impl View for TextArea {
debug!("{:?}", self.rows);
let scroll_width = if self.rows.len() > constraint.y { 1 } else { 0 };
Vec2::new(
scroll_width + 1 + self.rows.iter().map(|r| r.width).max().unwrap_or(1),
scroll_width + 1
+ self.rows.iter().map(|r| r.width).max().unwrap_or(1),
self.rows.len(),
)
}
@ -408,24 +404,23 @@ impl View for TextArea {
debug!("row: {:?}", row);
let text = &self.content[row.start..row.end];
debug!("row text: `{}`", text);
printer.with_effect(
effect,
|printer| { printer.print((0, 0), text); },
);
printer.with_effect(effect, |printer| {
printer.print((0, 0), text);
});
if printer.focused && i == self.selected_row() {
let cursor_offset = self.cursor - row.start;
let c = if cursor_offset == text.len() {
"_"
} else {
text[cursor_offset..].graphemes(true).next().expect(
"Found no char!",
)
text[cursor_offset..]
.graphemes(true)
.next()
.expect("Found no char!")
};
let offset = text[..cursor_offset].width();
printer.print((offset, 0), c);
}
});
});
}
@ -442,8 +437,8 @@ impl View for TextArea {
Event::Key(Key::End) => {
let row = self.selected_row();
self.cursor = self.rows[row].end;
if row + 1 < self.rows.len() &&
self.cursor == self.rows[row + 1].start
if row + 1 < self.rows.len()
&& self.cursor == self.rows[row + 1].start
{
self.move_left();
}
@ -455,7 +450,8 @@ impl View for TextArea {
}
Event::Key(Key::Up) if self.selected_row() > 0 => self.move_up(),
Event::Key(Key::Down)
if self.selected_row() + 1 < self.rows.len() => {
if self.selected_row() + 1 < self.rows.len() =>
{
self.move_down()
}
Event::Key(Key::PageUp) => self.page_up(),
@ -464,6 +460,26 @@ impl View for TextArea {
Event::Key(Key::Right) if self.cursor < self.content.len() => {
self.move_right()
}
Event::Mouse {
event: MouseEvent::Press(_),
position,
offset,
} if position.fits_in_rect(
offset,
self.last_size
.map(|s| s.map(SizeCache::value))
.unwrap_or_else(Vec2::zero),
) =>
{
position.checked_sub(offset).map(|position| {
let y = position.y + self.scrollbase.start_line;
let x = position.x;
let row = &self.rows[y];
let content = &self.content[row.start..row.end];
self.cursor = row.start + simple_prefix(content, x).length;
});
}
_ => return EventResult::Ignored,
}

View File

@ -4,12 +4,10 @@ use XY;
use align::*;
use direction::Direction;
use event::*;
use unicode_width::UnicodeWidthStr;
use utils::{LinesIterator, Row};
use vec::Vec2;
use view::{SizeCache, View, ScrollBase, ScrollStrategy};
use view::{ScrollBase, ScrollStrategy, SizeCache, View};
/// A simple view showing a fixed text
pub struct TextView {
@ -181,9 +179,9 @@ impl TextView {
// First attempt: naively hope that we won't need a scrollbar_width
// (This means we try to use the entire available width for text).
self.rows = LinesIterator::new(strip_last_newline(&self.content),
size.x)
.collect();
self.rows =
LinesIterator::new(strip_last_newline(&self.content), size.x)
.collect();
// Width taken by the scrollbar. Without a scrollbar, it's 0.
let mut scrollbar_width = 0;
@ -241,11 +239,10 @@ impl TextView {
impl View for TextView {
fn draw(&self, printer: &Printer) {
let h = self.rows.len();
// If the content is smaller than the view, align it somewhere.
let offset = self.align.v.get_offset(h, printer.size.y);
let printer =
&printer.sub_printer(Vec2::new(0, offset), printer.size, true);
let printer = &printer.offset((0, offset), true);
self.scrollbase.draw(printer, |printer, i| {
let row = &self.rows[i];
@ -261,14 +258,62 @@ impl View for TextView {
return EventResult::Ignored;
}
// We know we are scrollable, otherwise the event would just be ignored.
match event {
Event::Key(Key::Home) => self.scrollbase.scroll_top(),
Event::Key(Key::End) => self.scrollbase.scroll_bottom(),
Event::Key(Key::Up) if self.scrollbase.can_scroll_up() => {
self.scrollbase.scroll_up(1)
}
Event::Key(Key::Down) if self.scrollbase
.can_scroll_down() => self.scrollbase.scroll_down(1),
Event::Key(Key::Down) if self.scrollbase.can_scroll_down() => {
self.scrollbase.scroll_down(1)
}
Event::Mouse {
event: MouseEvent::WheelDown,
..
} if self.scrollbase.can_scroll_down() =>
{
self.scrollbase.scroll_down(5)
}
Event::Mouse {
event: MouseEvent::WheelUp,
..
} if self.scrollbase.can_scroll_up() =>
{
self.scrollbase.scroll_up(5)
}
Event::Mouse {
event: MouseEvent::Press(MouseButton::Left),
position,
offset,
} if position
.checked_sub(offset)
.and_then(|position| {
self.width.map(
|width| self.scrollbase.start_drag(position, width),
)
})
.unwrap_or(false) =>
{
// Only consume the event if the mouse hits the scrollbar.
// Start scroll drag at the given position.
}
Event::Mouse {
event: MouseEvent::Hold(MouseButton::Left),
position,
offset,
} => {
// If the mouse is dragged, we always consume the event.
position
.checked_sub(offset)
.map(|position| self.scrollbase.drag(position));
}
Event::Mouse {
event: MouseEvent::Release(MouseButton::Left),
..
} => {
self.scrollbase.release_grab();
}
Event::Key(Key::PageDown) => self.scrollbase.scroll_down(10),
Event::Key(Key::PageUp) => self.scrollbase.scroll_up(10),
_ => return EventResult::Ignored,

View File

@ -1,7 +1,6 @@
use Printer;
use std::cell::Cell;
use vec::Vec2;
use view::{View, ViewWrapper};
use views::IdView;

View File

@ -1,9 +1,8 @@
use direction::Orientation;
use std::iter;
/// A generic structure with a value for each axis.
#[derive(Debug,Clone,Copy,PartialEq,Eq,Hash)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct XY<T> {
/// X-axis value
pub x: T,