mirror of
https://github.com/FliegendeWurst/cursive.git
synced 2024-11-10 03:10:41 +00:00
Merge branch 'mouse.2'
This commit is contained in:
commit
8ce817741e
@ -51,7 +51,7 @@ version = "0.11"
|
||||
|
||||
[dependencies.termion]
|
||||
optional = true
|
||||
version = "1.4.0"
|
||||
version = "1.5.0"
|
||||
|
||||
[dev-dependencies]
|
||||
rand = "0.3"
|
||||
|
10
build.rs
10
build.rs
@ -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",
|
||||
]);
|
||||
}
|
||||
|
@ -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), "+");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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();
|
||||
|
@ -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()),
|
||||
);
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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" })
|
||||
});
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
@ -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),
|
||||
|
@ -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
542
src/cursive.rs
Normal 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();
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
28
src/div.rs
28
src/div.rs
@ -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
|
||||
}
|
||||
}
|
||||
|
112
src/event.rs
112
src/event.rs
@ -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)
|
||||
|
536
src/lib.rs
536
src/lib.rs
@ -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();
|
||||
}
|
||||
}
|
||||
|
40
src/menu.rs
40
src/menu.rs
@ -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.
|
||||
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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() {
|
||||
|
@ -10,6 +10,5 @@
|
||||
|
||||
#[doc(no_inline)]
|
||||
pub use With;
|
||||
|
||||
#[doc(no_inline)]
|
||||
pub use view::{Boxable, Finder, Identifiable, View};
|
||||
|
11
src/utf8.rs
11
src/utf8.rs
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -1,5 +1,4 @@
|
||||
use std::io::{self, Read};
|
||||
|
||||
use views::Counter;
|
||||
|
||||
/// Wrapper around a `Read` that reports the progress made.
|
||||
|
42
src/vec.rs
42
src/vec.rs
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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>>() {
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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)));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -1,7 +1,6 @@
|
||||
use Printer;
|
||||
use std::cell::Cell;
|
||||
use vec::Vec2;
|
||||
|
||||
use view::{View, ViewWrapper};
|
||||
use views::IdView;
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user