Add mouse support to pancurses

This commit is contained in:
Alexandre Bury 2017-11-19 00:40:56 -08:00
parent ee7170c66b
commit 0b5b6ceace
5 changed files with 268 additions and 40 deletions

View File

@ -1,10 +1,21 @@
[package] [package]
authors = ["Alexandre Bury <alexandre.bury@gmail.com>"] authors = ["Alexandre Bury <alexandre.bury@gmail.com>"]
categories = ["command-line-interface", "gui"] categories = [
"command-line-interface",
"gui",
]
description = "A TUI (Text User Interface) library focused on ease-of-use." description = "A TUI (Text User Interface) library focused on ease-of-use."
documentation = "https://gyscos.github.io/Cursive/cursive/index.html" documentation = "https://gyscos.github.io/Cursive/cursive/index.html"
exclude = ["doc/**", "assets/**", "examples/**"] exclude = [
keywords = ["ncurses", "TUI", "UI"] "doc/**",
"assets/**",
"examples/**",
]
keywords = [
"ncurses",
"TUI",
"UI",
]
license = "MIT" license = "MIT"
name = "cursive" name = "cursive"
readme = "Readme.md" readme = "Readme.md"
@ -14,7 +25,6 @@ version = "0.7.5-alpha.0"
[badges.travis-ci] [badges.travis-ci]
repository = "gyscos/Cursive" repository = "gyscos/Cursive"
[dependencies] [dependencies]
log = "0.3" log = "0.3"
num = "0.1" num = "0.1"
@ -24,6 +34,10 @@ toml = "0.4"
unicode-segmentation = "1.0" unicode-segmentation = "1.0"
unicode-width = "0.1" unicode-width = "0.1"
[dependencies.bear-lib-terminal]
optional = true
version = "1.3.1"
[dependencies.chan] [dependencies.chan]
optional = true optional = true
version = "0.1.18" version = "0.1.18"
@ -32,10 +46,6 @@ version = "0.1.18"
optional = true optional = true
version = "0.3" version = "0.3"
[dependencies.bear-lib-terminal]
optional = true
version = "1.3.1"
[dependencies.ncurses] [dependencies.ncurses]
features = ["wide"] features = ["wide"]
optional = true optional = true
@ -54,12 +64,15 @@ version = "1.5.0"
rand = "0.3" rand = "0.3"
[features] [features]
default = ["ncurses-backend"]
ncurses-backend = ["ncurses"]
termion-backend = ["termion", "chan", "chan-signal"]
pancurses-backend = ["pancurses"]
blt-backend = ["bear-lib-terminal"] blt-backend = ["bear-lib-terminal"]
default = ["ncurses-backend"]
ncurses-backend = ["ncurses"]
pancurses-backend = ["pancurses"]
termion-backend = [
"termion",
"chan",
"chan-signal",
]
[lib] [lib]
name = "cursive" name = "cursive"

View File

@ -4,6 +4,7 @@ use cursive::{Cursive, Printer};
use cursive::event::{Event, EventResult}; use cursive::event::{Event, EventResult};
use cursive::traits::*; use cursive::traits::*;
fn main() { fn main() {
let mut siv = Cursive::new(); let mut siv = Cursive::new();

View File

@ -10,6 +10,9 @@ mod pan;
#[cfg(feature = "pancurses")] #[cfg(feature = "pancurses")]
pub use self::pan::*; pub use self::pan::*;
fn split_u32(code: i32) -> Vec<u8> {
(0..4).map(|i| ((code >> (8 * i)) & 0xFF) as u8).collect()
}
fn find_closest(color: &Color) -> i16 { fn find_closest(color: &Color) -> i16 {
match *color { match *color {

View File

@ -1,6 +1,7 @@
extern crate ncurses; extern crate ncurses;
use self::super::find_closest; use self::ncurses::mmask_t;
use self::super::{find_closest, split_u32};
use backend; use backend;
use event::{Event, Key, MouseButton, MouseEvent}; use event::{Event, Key, MouseButton, MouseEvent};
use std::cell::{Cell, RefCell}; use std::cell::{Cell, RefCell};
@ -78,17 +79,17 @@ impl Concrete {
// eprintln!("{:032b}", mevent.bstate); // eprintln!("{:032b}", mevent.bstate);
// Currently unused // Currently unused
let _shift = (mevent.bstate let _shift = (mevent.bstate
& ncurses::BUTTON_SHIFT as ncurses::mmask_t) & ncurses::BUTTON_SHIFT as mmask_t)
!= 0; != 0;
let _alt = let _alt =
(mevent.bstate & ncurses::BUTTON_ALT as ncurses::mmask_t) != 0; (mevent.bstate & ncurses::BUTTON_ALT as mmask_t) != 0;
let _ctrl = (mevent.bstate let _ctrl = (mevent.bstate
& ncurses::BUTTON_CTRL as ncurses::mmask_t) & ncurses::BUTTON_CTRL as mmask_t)
!= 0; != 0;
mevent.bstate &= !(ncurses::BUTTON_SHIFT | ncurses::BUTTON_ALT mevent.bstate &= !(ncurses::BUTTON_SHIFT | ncurses::BUTTON_ALT
| ncurses::BUTTON_CTRL) | ncurses::BUTTON_CTRL)
as ncurses::mmask_t; as mmask_t;
let make_event = |event| { let make_event = |event| {
Event::Mouse { Event::Mouse {
@ -99,7 +100,7 @@ impl Concrete {
}; };
if mevent.bstate if mevent.bstate
== ncurses::REPORT_MOUSE_POSITION as ncurses::mmask_t == ncurses::REPORT_MOUSE_POSITION as mmask_t
{ {
// The event is either a mouse drag event, // The event is either a mouse drag event,
// or a weird double-release event. :S // or a weird double-release event. :S
@ -117,7 +118,7 @@ impl Concrete {
bare_event ^= single_event; bare_event ^= single_event;
// Process single_event // Process single_event
get_event(single_event as i32, |e| if event.is_none() { on_mouse_event(single_event as i32, |e| if event.is_none() {
event = Some(e); event = Some(e);
} else { } else {
self.event_queue.push(make_event(e)); self.event_queue.push(make_event(e));
@ -256,11 +257,7 @@ impl Concrete {
c @ 1...25 => Event::CtrlChar((b'a' + (c - 1) as u8) as char), c @ 1...25 => Event::CtrlChar((b'a' + (c - 1) as u8) as char),
other => { other => {
// Split the i32 into 4 bytes // Split the i32 into 4 bytes
Event::Unknown( Event::Unknown(split_u32(other))
(0..4)
.map(|i| ((other >> (8 * i)) & 0xFF) as u8)
.collect(),
)
} }
} }
} }
@ -286,7 +283,7 @@ impl backend::Backend for Concrete {
// Listen to all mouse events. // Listen to all mouse events.
ncurses::mousemask( ncurses::mousemask(
(ncurses::ALL_MOUSE_EVENTS | ncurses::REPORT_MOUSE_POSITION) (ncurses::ALL_MOUSE_EVENTS | ncurses::REPORT_MOUSE_POSITION)
as ncurses::mmask_t, as mmask_t,
None, None,
); );
ncurses::noecho(); ncurses::noecho();
@ -395,7 +392,7 @@ impl backend::Backend for Concrete {
} }
/// Returns the Key enum corresponding to the given ncurses event. /// Returns the Key enum corresponding to the given ncurses event.
fn get_button(bare_event: i32) -> MouseButton { fn get_mouse_button(bare_event: i32) -> MouseButton {
match bare_event { match bare_event {
ncurses::BUTTON1_RELEASED | ncurses::BUTTON1_RELEASED |
ncurses::BUTTON1_PRESSED | ncurses::BUTTON1_PRESSED |
@ -433,11 +430,11 @@ fn get_button(bare_event: i32) -> MouseButton {
/// the returned Vec will include those queued events. /// the returned Vec will include those queued events.
/// ///
/// The main event is returned separately to avoid allocation in most cases. /// The main event is returned separately to avoid allocation in most cases.
fn get_event<F>(bare_event: i32, mut f: F) fn on_mouse_event<F>(bare_event: i32, mut f: F)
where where
F: FnMut(MouseEvent), F: FnMut(MouseEvent),
{ {
let button = get_button(bare_event); let button = get_mouse_button(bare_event);
match bare_event { match bare_event {
ncurses::BUTTON4_PRESSED => f(MouseEvent::WheelUp), ncurses::BUTTON4_PRESSED => f(MouseEvent::WheelUp),
ncurses::BUTTON5_PRESSED => f(MouseEvent::WheelDown), ncurses::BUTTON5_PRESSED => f(MouseEvent::WheelDown),

View File

@ -1,17 +1,22 @@
extern crate pancurses; extern crate pancurses;
use self::super::find_closest; use self::pancurses::mmask_t;
use self::super::{find_closest, split_u32};
use backend; use backend;
use event::{Event, Key}; use event::{Event, Key, MouseButton, MouseEvent};
use std::cell::{Cell, RefCell}; use std::cell::{Cell, RefCell};
use std::collections::HashMap; use std::collections::HashMap;
use theme::{Color, ColorPair, Effect}; use theme::{Color, ColorPair, Effect};
use utf8; use utf8;
use vec::Vec2;
pub struct Concrete { pub struct Concrete {
current_style: Cell<ColorPair>, current_style: Cell<ColorPair>,
pairs: RefCell<HashMap<ColorPair, i32>>, pairs: RefCell<HashMap<ColorPair, i32>>,
window: pancurses::Window, window: pancurses::Window,
last_mouse_button: Option<MouseButton>,
event_queue: Vec<Event>,
} }
impl Concrete { impl Concrete {
@ -61,23 +66,102 @@ impl Concrete {
let style = pancurses::COLOR_PAIR(i as pancurses::chtype); let style = pancurses::COLOR_PAIR(i as pancurses::chtype);
self.window.attron(style); self.window.attron(style);
} }
fn parse_mouse_event(&mut self) -> Event {
let mut mevent = match pancurses::getmouse() {
Err(code) => return Event::Unknown(split_u32(code)),
Ok(event) => event,
};
let _shift =
(mevent.bstate & pancurses::BUTTON_SHIFT as mmask_t) != 0;
let _alt =
(mevent.bstate & pancurses::BUTTON_ALT as mmask_t) != 0;
let _ctrl =
(mevent.bstate & pancurses::BUTTON_CTRL as mmask_t) != 0;
mevent.bstate &= !(pancurses::BUTTON_SHIFT | pancurses::BUTTON_ALT
| pancurses::BUTTON_CTRL)
as 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 == pancurses::REPORT_MOUSE_POSITION as 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(|| {
debug!("We got a mouse drag, but no last mouse pressed?");
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
on_mouse_event(single_event, |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![])
}
}
}
} }
impl backend::Backend for Concrete { impl backend::Backend for Concrete {
fn init() -> Self { fn init() -> Self {
let window = pancurses::initscr();
::std::env::set_var("ESCDELAY", "25"); ::std::env::set_var("ESCDELAY", "25");
let window = pancurses::initscr();
window.keypad(true); window.keypad(true);
pancurses::noecho(); pancurses::noecho();
pancurses::cbreak(); pancurses::cbreak();
pancurses::start_color(); pancurses::start_color();
pancurses::use_default_colors(); pancurses::use_default_colors();
pancurses::curs_set(0); pancurses::curs_set(0);
pancurses::mouseinterval(0);
pancurses::mousemask(
pancurses::ALL_MOUSE_EVENTS | pancurses::REPORT_MOUSE_POSITION,
::std::ptr::null_mut(),
);
// 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 { Concrete {
current_style: Cell::new(ColorPair::from_256colors(0, 0)), current_style: Cell::new(ColorPair::from_256colors(0, 0)),
pairs: RefCell::new(HashMap::new()), pairs: RefCell::new(HashMap::new()),
window: window, window: window,
last_mouse_button: None,
event_queue: Vec::new(),
} }
} }
@ -136,8 +220,7 @@ impl backend::Backend for Concrete {
} }
fn poll_event(&mut self) -> Event { fn poll_event(&mut self) -> Event {
// TODO: there seems to not be any indication self.event_queue.pop().unwrap_or_else(|| {
// of Ctrl/Alt/Shift in these :v
if let Some(ev) = self.window.getch() { if let Some(ev) = self.window.getch() {
match ev { match ev {
pancurses::Input::Character('\n') => Event::Key(Key::Enter), pancurses::Input::Character('\n') => Event::Key(Key::Enter),
@ -163,6 +246,9 @@ impl backend::Backend for Concrete {
}).unwrap(), }).unwrap(),
) )
} }
pancurses::Input::Character(c) if (c as u32) <= 26 => {
Event::CtrlChar((b'a' - 1 + c as u8) as char)
}
pancurses::Input::Character(c) => { pancurses::Input::Character(c) => {
let mut bytes = [0u8; 4]; let mut bytes = [0u8; 4];
Event::Unknown( Event::Unknown(
@ -171,11 +257,53 @@ impl backend::Backend for Concrete {
} }
// TODO: Some key combos are not recognized by pancurses, // TODO: Some key combos are not recognized by pancurses,
// but are sent as Unknown. We could still parse them here. // but are sent as Unknown. We could still parse them here.
pancurses::Input::Unknown(other) => Event::Unknown( pancurses::Input::Unknown(code) => match code {
(0..4) 220 => Event::Ctrl(Key::Del),
.map(|i| ((other >> (8 * i)) & 0xFF) as u8)
.collect(), 224 => Event::Alt(Key::Down),
), 225 => Event::AltShift(Key::Down),
226 => Event::Ctrl(Key::Down),
227 => Event::CtrlShift(Key::Down),
229 => Event::Alt(Key::End),
230 => Event::AltShift(Key::End),
231 => Event::Ctrl(Key::End),
232 => Event::CtrlShift(Key::End),
235 => Event::Alt(Key::Home),
236 => Event::AltShift(Key::Home),
237 => Event::Ctrl(Key::Home),
238 => Event::CtrlShift(Key::Home),
246 => Event::Alt(Key::Left),
247 => Event::AltShift(Key::Left),
248 => Event::Ctrl(Key::Left),
249 => Event::CtrlShift(Key::Left),
251 => Event::Alt(Key::PageDown),
252 => Event::AltShift(Key::PageDown),
253 => Event::Ctrl(Key::PageDown),
254 => Event::CtrlShift(Key::PageDown),
256 => Event::Alt(Key::PageUp),
257 => Event::AltShift(Key::PageUp),
258 => Event::Ctrl(Key::PageUp),
259 => Event::CtrlShift(Key::PageUp),
261 => Event::Alt(Key::Right),
262 => Event::AltShift(Key::Right),
263 => Event::Ctrl(Key::Right),
264 => Event::CtrlShift(Key::Right),
267 => Event::Alt(Key::Up),
268 => Event::AltShift(Key::Up),
269 => Event::Ctrl(Key::Up),
270 => Event::CtrlShift(Key::Up),
other => {
eprintln!("Unknown: {}", other);
Event::Unknown(split_u32(other))
}
},
// TODO: I honestly have no fucking idea what KeyCodeYes is // TODO: I honestly have no fucking idea what KeyCodeYes is
pancurses::Input::KeyCodeYes => Event::Refresh, pancurses::Input::KeyCodeYes => Event::Refresh,
pancurses::Input::KeyBreak => Event::Key(Key::PauseBreak), pancurses::Input::KeyBreak => Event::Key(Key::PauseBreak),
@ -282,7 +410,7 @@ impl backend::Backend for Concrete {
pancurses::Input::KeyResize => Event::WindowResize, pancurses::Input::KeyResize => Event::WindowResize,
pancurses::Input::KeyEvent => Event::Refresh, pancurses::Input::KeyEvent => Event::Refresh,
// TODO: mouse support // TODO: mouse support
pancurses::Input::KeyMouse => Event::Refresh, pancurses::Input::KeyMouse => self.parse_mouse_event(),
pancurses::Input::KeyA1 => Event::Refresh, pancurses::Input::KeyA1 => Event::Refresh,
pancurses::Input::KeyA3 => Event::Refresh, pancurses::Input::KeyA3 => Event::Refresh,
pancurses::Input::KeyB2 => Event::Key(Key::NumpadCenter), pancurses::Input::KeyB2 => Event::Key(Key::NumpadCenter),
@ -292,6 +420,7 @@ impl backend::Backend for Concrete {
} else { } else {
Event::Refresh Event::Refresh
} }
})
} }
fn set_refresh_rate(&mut self, fps: u32) { fn set_refresh_rate(&mut self, fps: u32) {
@ -302,3 +431,88 @@ impl backend::Backend for Concrete {
} }
} }
} }
/// 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 on_mouse_event<F>(bare_event: u32, mut f: F)
where
F: FnMut(MouseEvent),
{
let button = get_mouse_button(bare_event);
match bare_event {
pancurses::BUTTON4_PRESSED => f(MouseEvent::WheelUp),
pancurses::BUTTON5_PRESSED => f(MouseEvent::WheelDown),
pancurses::BUTTON1_RELEASED |
pancurses::BUTTON2_RELEASED |
pancurses::BUTTON3_RELEASED |
pancurses::BUTTON4_RELEASED |
pancurses::BUTTON5_RELEASED => f(MouseEvent::Release(button)),
pancurses::BUTTON1_PRESSED |
pancurses::BUTTON2_PRESSED |
pancurses::BUTTON3_PRESSED => f(MouseEvent::Press(button)),
pancurses::BUTTON1_CLICKED |
pancurses::BUTTON2_CLICKED |
pancurses::BUTTON3_CLICKED |
pancurses::BUTTON4_CLICKED |
pancurses::BUTTON5_CLICKED => {
f(MouseEvent::Press(button));
f(MouseEvent::Release(button));
}
// Well, we disabled click detection
pancurses::BUTTON1_DOUBLE_CLICKED |
pancurses::BUTTON2_DOUBLE_CLICKED |
pancurses::BUTTON3_DOUBLE_CLICKED |
pancurses::BUTTON4_DOUBLE_CLICKED |
pancurses::BUTTON5_DOUBLE_CLICKED => for _ in 0..2 {
f(MouseEvent::Press(button));
f(MouseEvent::Release(button));
},
pancurses::BUTTON1_TRIPLE_CLICKED |
pancurses::BUTTON2_TRIPLE_CLICKED |
pancurses::BUTTON3_TRIPLE_CLICKED |
pancurses::BUTTON4_TRIPLE_CLICKED |
pancurses::BUTTON5_TRIPLE_CLICKED => for _ in 0..3 {
f(MouseEvent::Press(button));
f(MouseEvent::Release(button));
},
_ => debug!("Unknown event: {:032b}", bare_event),
}
}
/// Returns the Key enum corresponding to the given pancurses event.
fn get_mouse_button(bare_event: u32) -> MouseButton {
match bare_event {
pancurses::BUTTON1_RELEASED |
pancurses::BUTTON1_PRESSED |
pancurses::BUTTON1_CLICKED |
pancurses::BUTTON1_DOUBLE_CLICKED |
pancurses::BUTTON1_TRIPLE_CLICKED => MouseButton::Left,
pancurses::BUTTON2_RELEASED |
pancurses::BUTTON2_PRESSED |
pancurses::BUTTON2_CLICKED |
pancurses::BUTTON2_DOUBLE_CLICKED |
pancurses::BUTTON2_TRIPLE_CLICKED => MouseButton::Middle,
pancurses::BUTTON3_RELEASED |
pancurses::BUTTON3_PRESSED |
pancurses::BUTTON3_CLICKED |
pancurses::BUTTON3_DOUBLE_CLICKED |
pancurses::BUTTON3_TRIPLE_CLICKED => MouseButton::Right,
pancurses::BUTTON4_RELEASED |
pancurses::BUTTON4_PRESSED |
pancurses::BUTTON4_CLICKED |
pancurses::BUTTON4_DOUBLE_CLICKED |
pancurses::BUTTON4_TRIPLE_CLICKED => MouseButton::Button4,
pancurses::BUTTON5_RELEASED |
pancurses::BUTTON5_PRESSED |
pancurses::BUTTON5_CLICKED |
pancurses::BUTTON5_DOUBLE_CLICKED |
pancurses::BUTTON5_TRIPLE_CLICKED => MouseButton::Button5,
_ => MouseButton::Other,
}
}