From 0b5b6ceace9a097a8271ff9a6c5bbad3b7445cba Mon Sep 17 00:00:00 2001 From: Alexandre Bury Date: Sun, 19 Nov 2017 00:40:56 -0800 Subject: [PATCH] Add mouse support to pancurses --- Cargo.toml | 39 ++++--- examples/key_codes.rs | 1 + src/backend/curses/mod.rs | 3 + src/backend/curses/n.rs | 29 +++-- src/backend/curses/pan.rs | 236 ++++++++++++++++++++++++++++++++++++-- 5 files changed, 268 insertions(+), 40 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d856971..fbaa8bb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,21 @@ [package] authors = ["Alexandre Bury "] -categories = ["command-line-interface", "gui"] +categories = [ + "command-line-interface", + "gui", +] description = "A TUI (Text User Interface) library focused on ease-of-use." documentation = "https://gyscos.github.io/Cursive/cursive/index.html" -exclude = ["doc/**", "assets/**", "examples/**"] -keywords = ["ncurses", "TUI", "UI"] +exclude = [ + "doc/**", + "assets/**", + "examples/**", +] +keywords = [ + "ncurses", + "TUI", + "UI", +] license = "MIT" name = "cursive" readme = "Readme.md" @@ -14,7 +25,6 @@ version = "0.7.5-alpha.0" [badges.travis-ci] repository = "gyscos/Cursive" - [dependencies] log = "0.3" num = "0.1" @@ -24,6 +34,10 @@ toml = "0.4" unicode-segmentation = "1.0" unicode-width = "0.1" +[dependencies.bear-lib-terminal] +optional = true +version = "1.3.1" + [dependencies.chan] optional = true version = "0.1.18" @@ -32,10 +46,6 @@ version = "0.1.18" optional = true version = "0.3" -[dependencies.bear-lib-terminal] -optional = true -version = "1.3.1" - [dependencies.ncurses] features = ["wide"] optional = true @@ -54,12 +64,15 @@ version = "1.5.0" rand = "0.3" [features] -default = ["ncurses-backend"] - -ncurses-backend = ["ncurses"] -termion-backend = ["termion", "chan", "chan-signal"] -pancurses-backend = ["pancurses"] blt-backend = ["bear-lib-terminal"] +default = ["ncurses-backend"] +ncurses-backend = ["ncurses"] +pancurses-backend = ["pancurses"] +termion-backend = [ + "termion", + "chan", + "chan-signal", +] [lib] name = "cursive" diff --git a/examples/key_codes.rs b/examples/key_codes.rs index 83c3032..7ea8f65 100644 --- a/examples/key_codes.rs +++ b/examples/key_codes.rs @@ -4,6 +4,7 @@ use cursive::{Cursive, Printer}; use cursive::event::{Event, EventResult}; use cursive::traits::*; + fn main() { let mut siv = Cursive::new(); diff --git a/src/backend/curses/mod.rs b/src/backend/curses/mod.rs index 0c1264c..65fc9de 100644 --- a/src/backend/curses/mod.rs +++ b/src/backend/curses/mod.rs @@ -10,6 +10,9 @@ mod pan; #[cfg(feature = "pancurses")] pub use self::pan::*; +fn split_u32(code: i32) -> Vec { + (0..4).map(|i| ((code >> (8 * i)) & 0xFF) as u8).collect() +} fn find_closest(color: &Color) -> i16 { match *color { diff --git a/src/backend/curses/n.rs b/src/backend/curses/n.rs index 542cd24..027ff00 100644 --- a/src/backend/curses/n.rs +++ b/src/backend/curses/n.rs @@ -1,6 +1,7 @@ extern crate ncurses; -use self::super::find_closest; +use self::ncurses::mmask_t; +use self::super::{find_closest, split_u32}; use backend; use event::{Event, Key, MouseButton, MouseEvent}; use std::cell::{Cell, RefCell}; @@ -78,17 +79,17 @@ impl Concrete { // eprintln!("{:032b}", mevent.bstate); // Currently unused let _shift = (mevent.bstate - & ncurses::BUTTON_SHIFT as ncurses::mmask_t) + & ncurses::BUTTON_SHIFT as mmask_t) != 0; 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 - & ncurses::BUTTON_CTRL as ncurses::mmask_t) + & ncurses::BUTTON_CTRL as mmask_t) != 0; mevent.bstate &= !(ncurses::BUTTON_SHIFT | ncurses::BUTTON_ALT | ncurses::BUTTON_CTRL) - as ncurses::mmask_t; + as mmask_t; let make_event = |event| { Event::Mouse { @@ -99,7 +100,7 @@ impl Concrete { }; 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, // or a weird double-release event. :S @@ -117,7 +118,7 @@ impl Concrete { bare_event ^= 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); } else { 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), other => { // Split the i32 into 4 bytes - Event::Unknown( - (0..4) - .map(|i| ((other >> (8 * i)) & 0xFF) as u8) - .collect(), - ) + Event::Unknown(split_u32(other)) } } } @@ -286,7 +283,7 @@ impl backend::Backend for Concrete { // Listen to all mouse events. ncurses::mousemask( (ncurses::ALL_MOUSE_EVENTS | ncurses::REPORT_MOUSE_POSITION) - as ncurses::mmask_t, + as mmask_t, None, ); ncurses::noecho(); @@ -395,7 +392,7 @@ impl backend::Backend for Concrete { } /// 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 { ncurses::BUTTON1_RELEASED | ncurses::BUTTON1_PRESSED | @@ -433,11 +430,11 @@ fn get_button(bare_event: i32) -> MouseButton { /// the returned Vec will include those queued events. /// /// The main event is returned separately to avoid allocation in most cases. -fn get_event(bare_event: i32, mut f: F) +fn on_mouse_event(bare_event: i32, mut f: F) where F: FnMut(MouseEvent), { - let button = get_button(bare_event); + let button = get_mouse_button(bare_event); match bare_event { ncurses::BUTTON4_PRESSED => f(MouseEvent::WheelUp), ncurses::BUTTON5_PRESSED => f(MouseEvent::WheelDown), diff --git a/src/backend/curses/pan.rs b/src/backend/curses/pan.rs index 78f62e2..c067b08 100644 --- a/src/backend/curses/pan.rs +++ b/src/backend/curses/pan.rs @@ -1,17 +1,22 @@ extern crate pancurses; -use self::super::find_closest; +use self::pancurses::mmask_t; +use self::super::{find_closest, split_u32}; use backend; -use event::{Event, Key}; +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, pairs: RefCell>, window: pancurses::Window, + + last_mouse_button: Option, + event_queue: Vec, } impl Concrete { @@ -61,23 +66,102 @@ impl Concrete { let style = pancurses::COLOR_PAIR(i as pancurses::chtype); 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 { fn init() -> Self { - let window = pancurses::initscr(); ::std::env::set_var("ESCDELAY", "25"); + + let window = pancurses::initscr(); window.keypad(true); pancurses::noecho(); pancurses::cbreak(); pancurses::start_color(); pancurses::use_default_colors(); 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 { current_style: Cell::new(ColorPair::from_256colors(0, 0)), pairs: RefCell::new(HashMap::new()), 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 { - // TODO: there seems to not be any indication - // of Ctrl/Alt/Shift in these :v + self.event_queue.pop().unwrap_or_else(|| { if let Some(ev) = self.window.getch() { match ev { pancurses::Input::Character('\n') => Event::Key(Key::Enter), @@ -163,6 +246,9 @@ impl backend::Backend for Concrete { }).unwrap(), ) } + pancurses::Input::Character(c) if (c as u32) <= 26 => { + Event::CtrlChar((b'a' - 1 + c as u8) as char) + } pancurses::Input::Character(c) => { let mut bytes = [0u8; 4]; Event::Unknown( @@ -171,11 +257,53 @@ impl backend::Backend for Concrete { } // 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(code) => match code { + 220 => Event::Ctrl(Key::Del), + + 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 pancurses::Input::KeyCodeYes => Event::Refresh, pancurses::Input::KeyBreak => Event::Key(Key::PauseBreak), @@ -282,7 +410,7 @@ impl backend::Backend for Concrete { pancurses::Input::KeyResize => Event::WindowResize, pancurses::Input::KeyEvent => Event::Refresh, // TODO: mouse support - pancurses::Input::KeyMouse => Event::Refresh, + pancurses::Input::KeyMouse => self.parse_mouse_event(), pancurses::Input::KeyA1 => Event::Refresh, pancurses::Input::KeyA3 => Event::Refresh, pancurses::Input::KeyB2 => Event::Key(Key::NumpadCenter), @@ -292,6 +420,7 @@ impl backend::Backend for Concrete { } else { Event::Refresh } + }) } 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(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, + } +} +