diff --git a/src/cursor.rs b/src/cursor.rs index 96a8000..5522474 100644 --- a/src/cursor.rs +++ b/src/cursor.rs @@ -2,7 +2,8 @@ use cairo; use ui_model::Color; use ui::UiMutex; use shell; -use shell::NvimMode; +use mode; +use nvim; use nvim::{RepaintMode, RedrawEvents}; use std::sync::{Arc, Weak}; @@ -123,7 +124,63 @@ impl Cursor { let current_point = ctx.get_current_point(); ctx.set_source_rgba(1.0 - bg.0, 1.0 - bg.1, 1.0 - bg.2, 0.6 * state.alpha.0); - let cursor_width = if shell.mode == NvimMode::Insert { + let (y, width, height) = + cursor_rect(&shell.mode, char_width, line_height, line_y, double_width); + + ctx.rectangle(current_point.0, y, width, height); + if state.anim_phase == AnimPhase::NoFocus { + ctx.stroke(); + } else { + ctx.fill(); + } + } +} + +fn cursor_rect(mode: &mode::Mode, + char_width: f64, + line_height: f64, + line_y: f64, + double_width: bool) + -> (f64, f64, f64) { + if let Some(mode_info) = mode.mode_info() { + match mode_info.cursor_shape() { + None | + Some(&nvim::CursorShape::Unknown) | + Some(&nvim::CursorShape::Block) => { + let cursor_width = if double_width { + char_width * 2.0 + } else { + char_width + }; + (line_y, cursor_width, line_height) + } + Some(&nvim::CursorShape::Vertical) => { + let cell_percentage = mode_info.cell_percentage(); + let cursor_width = if cell_percentage > 0 { + (char_width * cell_percentage as f64) / 100.0 + } else { + char_width + }; + (line_y, cursor_width, line_height) + } + Some(&nvim::CursorShape::Horizontal) => { + let cell_percentage = mode_info.cell_percentage(); + let cursor_width = if double_width { + char_width * 2.0 + } else { + char_width + }; + + if cell_percentage > 0 { + let height = (line_height * cell_percentage as f64) / 100.0; + (line_y + line_height - height, cursor_width, height) + } else { + (line_y, cursor_width, line_height) + } + } + } + } else { + let cursor_width = if mode.is(&mode::NvimMode::Insert) { char_width / 5.0 } else { if double_width { @@ -133,15 +190,9 @@ impl Cursor { } }; - ctx.rectangle(current_point.0, line_y, cursor_width, line_height); - if state.anim_phase == AnimPhase::NoFocus { - ctx.stroke(); - } else { - ctx.fill(); - } + (line_y, cursor_width, line_height) } } - fn anim_step(state: &Arc>) -> glib::Continue { let mut mut_state = state.borrow_mut(); @@ -201,3 +252,62 @@ impl Drop for Cursor { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_cursor_rect_horizontal() { + let mut mode = mode::Mode::new(); + let mode_info = nvim::ModeInfo::new(&vec![(From::from("cursor_shape"), + From::from("horizontal")), + (From::from("cell_percentage"), From::from(25))]); + mode.update("insert", 0); + mode.set_info(true, vec![mode_info.unwrap()]); + let char_width = 50.0; + let line_height = 30.0; + let line_y = 0.0; + + let (y, width, height) = cursor_rect(&mode, char_width, line_height, line_y, false); + assert_eq!(line_y + line_height - line_height / 4.0, y); + assert_eq!(char_width, width); + assert_eq!(line_height / 4.0, height); + } + + #[test] + fn test_cursor_rect_horizontal_doublewidth() { + let mut mode = mode::Mode::new(); + let mode_info = nvim::ModeInfo::new(&vec![(From::from("cursor_shape"), + From::from("horizontal")), + (From::from("cell_percentage"), From::from(25))]); + mode.update("insert", 0); + mode.set_info(true, vec![mode_info.unwrap()]); + let char_width = 50.0; + let line_height = 30.0; + let line_y = 0.0; + + let (y, width, height) = cursor_rect(&mode, char_width, line_height, line_y, true); + assert_eq!(line_y + line_height - line_height / 4.0, y); + assert_eq!(char_width * 2.0, width); + assert_eq!(line_height / 4.0, height); + } + + #[test] + fn test_cursor_rect_vertical() { + let mut mode = mode::Mode::new(); + let mode_info = nvim::ModeInfo::new(&vec![(From::from("cursor_shape"), + From::from("vertical")), + (From::from("cell_percentage"), From::from(25))]); + mode.update("insert", 0); + mode.set_info(true, vec![mode_info.unwrap()]); + let char_width = 50.0; + let line_height = 30.0; + let line_y = 0.0; + + let (y, width, height) = cursor_rect(&mode, char_width, line_height, line_y, false); + assert_eq!(line_y, y); + assert_eq!(char_width / 4.0, width); + assert_eq!(line_height, height); + } +} diff --git a/src/main.rs b/src/main.rs index 85fc87f..a9cab44 100644 --- a/src/main.rs +++ b/src/main.rs @@ -19,6 +19,8 @@ extern crate serde_derive; extern crate serde; extern crate toml; +mod value; +mod mode; mod ui_model; #[macro_use] mod ui; diff --git a/src/mode.rs b/src/mode.rs new file mode 100644 index 0000000..e8fdc60 --- /dev/null +++ b/src/mode.rs @@ -0,0 +1,52 @@ +use nvim; + +#[derive(PartialEq)] +pub enum NvimMode { + Normal, + Insert, + Other, +} + +pub struct Mode { + mode: NvimMode, + idx: usize, + info: Option>, +} + +impl Mode { + pub fn new() -> Self { + Mode { + mode: NvimMode::Normal, + idx: 0, + info: None, + } + } + + pub fn is(&self, mode: &NvimMode) -> bool { + self.mode == *mode + } + + pub fn mode_info(&self) -> Option<&nvim::ModeInfo> { + self.info + .as_ref() + .and_then(|i| i.get(self.idx)) + } + + pub fn update(&mut self, mode: &str, idx: usize) { + match mode { + "normal" => self.mode = NvimMode::Normal, + "insert" => self.mode = NvimMode::Insert, + _ => self.mode = NvimMode::Other, + } + + self.idx = idx; + } + + pub fn set_info(&mut self, cursor_style_enabled: bool, info: Vec) { + self.info = if cursor_style_enabled { + Some(info) + } else { + None + }; + } +} diff --git a/src/nvim.rs b/src/nvim.rs index f2af5c4..eabcae5 100644 --- a/src/nvim.rs +++ b/src/nvim.rs @@ -14,6 +14,8 @@ use ui_model::{ModelRect, ModelRectVec}; use shell; use glib; +use value::ValueMapExt; + pub trait RedrawEvents { fn on_cursor_goto(&mut self, row: u64, col: u64) -> RepaintMode; @@ -39,7 +41,7 @@ pub trait RedrawEvents { fn on_update_sp(&mut self, sp: i64) -> RepaintMode; - fn on_mode_change(&mut self, mode: &str) -> RepaintMode; + fn on_mode_change(&mut self, mode: &str, idx: u64) -> RepaintMode; fn on_mouse(&mut self, on: bool) -> RepaintMode; @@ -58,8 +60,13 @@ pub trait RedrawEvents { fn tabline_update(&mut self, selected: Tabpage, - tabs: Vec<(Tabpage, Option<&str>)>) + tabs: Vec<(Tabpage, Option)>) -> RepaintMode; + + fn mode_info_set(&mut self, + cursor_style_enabled: bool, + mode_info: Vec) + -> RepaintMode; } pub trait GuiApi { @@ -78,6 +85,90 @@ macro_rules! try_uint { ($exp:expr) => ($exp.as_u64().ok_or("Can't convert argument to u64".to_owned())?) } +macro_rules! try_bool { + ($exp:expr) => ($exp.as_bool().ok_or("Can't convert argument to bool".to_owned())?) +} + +macro_rules! map_array { + ($arg:expr, $err:expr, |$item:ident| $exp:expr) => ( + $arg.as_array() + .ok_or($err) + .and_then(|items| items.iter().map(|$item| { + $exp + }).collect::, _>>()) + ); + ($arg:expr, $err:expr, |$item:ident| {$exp:expr}) => ( + $arg.as_array() + .ok_or($err) + .and_then(|items| items.iter().map(|$item| { + $exp + }).collect::, _>>()) + ); +} + +#[derive(Debug, Clone)] +pub enum CursorShape { + Block, + Horizontal, + Vertical, + Unknown, +} + +impl CursorShape { + fn new(shape_code: &Value) -> Result { + let str_code = shape_code + .as_str() + .ok_or("Can't convert cursor shape to string".to_owned())?; + + Ok(match str_code { + "block" => CursorShape::Block, + "horizontal" => CursorShape::Horizontal, + "vertical" => CursorShape::Vertical, + _ => { + error!("Unknown cursor_shape {}", str_code); + CursorShape::Unknown + } + }) + } +} + +#[derive(Debug, Clone)] +pub struct ModeInfo { + cursor_shape: Option, + cell_percentage: Option, +} + +impl ModeInfo { + pub fn new(mode_info_arr: &Vec<(Value, Value)>) -> Result { + let mode_info_map = mode_info_arr.to_attrs_map()?; + + let cursor_shape = if let Some(shape) = mode_info_map.get("cursor_shape") { + Some(CursorShape::new(shape)?) + } else { + None + }; + + let cell_percentage = if let Some(cell_percentage) = mode_info_map.get("cell_percentage") { + cell_percentage.as_u64() + } else { + None + }; + + Ok(ModeInfo { + cursor_shape, + cell_percentage, + }) + } + + pub fn cursor_shape(&self) -> Option<&CursorShape> { + self.cursor_shape.as_ref() + } + + pub fn cell_percentage(&self) -> u64 { + self.cell_percentage.unwrap_or(0) + } +} + #[derive(Debug)] pub struct NvimInitError { source: Box, @@ -104,7 +195,7 @@ impl NvimInitError { } impl fmt::Display for NvimInitError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{:?}", self.source) } } @@ -305,23 +396,17 @@ fn call(ui: &mut shell::State, "update_bg" => ui.on_update_bg(try_int!(args[0])), "update_fg" => ui.on_update_fg(try_int!(args[0])), "update_sp" => ui.on_update_sp(try_int!(args[0])), - "mode_change" => ui.on_mode_change(try_str!(args[0])), + "mode_change" => ui.on_mode_change(try_str!(args[0]), try_uint!(args[1])), "mouse_on" => ui.on_mouse(true), "mouse_off" => ui.on_mouse(false), "busy_start" => ui.on_busy(true), "busy_stop" => ui.on_busy(false), "popupmenu_show" => { - let mut menu_items = Vec::new(); - - let items = args[0].as_array().ok_or("Error get menu list array")?; - for item in items { - let item_line: result::Result, &str> = item.as_array() - .ok_or("Error get menu item array")? - .iter() - .map(|col| col.as_str().ok_or("Error get menu column")) - .collect(); - menu_items.push(item_line?); - } + let menu_items = map_array!(args[0], "Error get menu list array", |item| { + map_array!(item, + "Error get menu item array", + |col| col.as_str().ok_or("Error get menu column")) + })?; ui.popupmenu_show(&menu_items, try_int!(args[1]), @@ -331,27 +416,34 @@ fn call(ui: &mut shell::State, "popupmenu_hide" => ui.popupmenu_hide(), "popupmenu_select" => ui.popupmenu_select(try_int!(args[0])), "tabline_update" => { - let tabs_in = args[1].as_array().ok_or("Error get tabline list")?; + let tabs_out = map_array!(args[1], "Error get tabline list".to_owned(), |tab| { + tab.as_map() + .ok_or("Error get map for tab".to_owned()) + .and_then(|tab_map| tab_map.to_attrs_map()) + .map(|tab_attrs| { + let name_attr = tab_attrs + .get("name") + .and_then(|n| n.as_str().map(|s| s.to_owned())); + let tab_attr = tab_attrs + .get("tab") + .map(|tab_id| Tabpage::new(tab_id.clone())) + .unwrap(); - let mut tabs_out = Vec::new(); - for tab in tabs_in { - let tab_attrs = tab.as_map().ok_or("Error get map for tab")?; - - let mut tab_attr = None; - let mut name_attr = None; - - for attr in tab_attrs { - let key = attr.0.as_str().ok_or("Error get key value")?; - if key == "tab" { - tab_attr = Some(Tabpage::new(attr.1.clone())); - } else if key == "name" { - name_attr = attr.1.as_str(); - } - } - tabs_out.push((tab_attr.unwrap(), name_attr)); - } + (tab_attr, name_attr) + }) + })?; ui.tabline_update(Tabpage::new(args[0].clone()), tabs_out) } + "mode_info_set" => { + let mode_info = map_array!(args[1], + "Error get array key value for mode_info".to_owned(), + |mi| { + mi.as_map() + .ok_or("Erro get map for mode_info".to_owned()) + .and_then(|mi_map| ModeInfo::new(mi_map)) + })?; + ui.mode_info_set(try_bool!(args[0]), mode_info) + } _ => { println!("Event {}({:?})", method, args); RepaintMode::Nothing @@ -438,7 +530,9 @@ impl NeovimClientWrapper { match *self { NeovimClientWrapper::Initialized(ref nvim) => nvim, NeovimClientWrapper::Uninitialized => panic!("Access to uninitialized neovim client"), - NeovimClientWrapper::Error => panic!("Access to neovim client that is not started due to some error"), + NeovimClientWrapper::Error => { + panic!("Access to neovim client that is not started due to some error") + } } } @@ -446,20 +540,20 @@ impl NeovimClientWrapper { match *self { NeovimClientWrapper::Initialized(ref mut nvim) => nvim, NeovimClientWrapper::Uninitialized => panic!("Access to uninitialized neovim client"), - NeovimClientWrapper::Error => panic!("Access to neovim client that is not started due to some error"), + NeovimClientWrapper::Error => { + panic!("Access to neovim client that is not started due to some error") + } } } } pub struct NeovimClient { - nvim: NeovimClientWrapper + nvim: NeovimClientWrapper, } impl NeovimClient { pub fn new() -> Self { - NeovimClient { - nvim: NeovimClientWrapper::Uninitialized, - } + NeovimClient { nvim: NeovimClientWrapper::Uninitialized } } pub fn set_nvim(&mut self, nvim: Neovim) { diff --git a/src/shell.rs b/src/shell.rs index 6260d1d..cdba5cd 100644 --- a/src/shell.rs +++ b/src/shell.rs @@ -28,6 +28,7 @@ use ui::UiMutex; use popup_menu::PopupMenu; use tabline::Tabline; use error; +use mode; const DEFAULT_FONT_NAME: &str = "DejaVu Sans Mono 12"; pub const MINIMUM_SUPPORTED_NVIM_VERSION: &str = "0.2"; @@ -44,21 +45,12 @@ macro_rules! idle_cb_call { ) } - -#[derive(PartialEq)] -pub enum NvimMode { - Normal, - Insert, - Other, -} - pub struct State { pub model: UiModel, bg_color: Color, fg_color: Color, sp_color: Color, cur_attrs: Option, - pub mode: NvimMode, mouse_enabled: bool, nvim: Rc>, font_desc: FontDescription, @@ -66,6 +58,8 @@ pub struct State { popup_menu: RefCell, settings: Rc>, + pub mode: mode::Mode, + stack: gtk::Stack, drawing_area: gtk::DrawingArea, tabs: Tabline, @@ -93,13 +87,15 @@ impl State { bg_color: COLOR_BLACK, fg_color: COLOR_WHITE, sp_color: COLOR_RED, - mode: NvimMode::Normal, mouse_enabled: true, font_desc: FontDescription::from_string(DEFAULT_FONT_NAME), cursor: None, popup_menu, settings: settings, + mode: mode::Mode::new(), + + // UI stack: gtk::Stack::new(), drawing_area, tabs: Tabline::new(), @@ -414,7 +410,7 @@ impl Shell { pub fn edit_paste(&self) { let state = self.state.borrow(); - let paste_command = if state.mode == NvimMode::Normal { + let paste_command = if state.mode.is(&mode::NvimMode::Normal) { "\"*p" } else { "\"*pa" @@ -949,13 +945,8 @@ impl RedrawEvents for State { RepaintMode::Nothing } - fn on_mode_change(&mut self, mode: &str) -> RepaintMode { - match mode { - "normal" => self.mode = NvimMode::Normal, - "insert" => self.mode = NvimMode::Insert, - _ => self.mode = NvimMode::Other, - } - + fn on_mode_change(&mut self, mode: &str, idx: u64) -> RepaintMode { + self.mode.update(mode, idx as usize); RepaintMode::Area(self.model.cur_point()) } @@ -1007,12 +998,20 @@ impl RedrawEvents for State { fn tabline_update(&mut self, selected: Tabpage, - tabs: Vec<(Tabpage, Option<&str>)>) + tabs: Vec<(Tabpage, Option)>) -> RepaintMode { self.tabs.update_tabs(&self.nvim, &selected, &tabs); RepaintMode::Nothing } + + + fn mode_info_set(&mut self, + cursor_style_enabled: bool, + mode_info: Vec) -> RepaintMode { + self.mode.set_info(cursor_style_enabled, mode_info); + RepaintMode::Nothing + } } impl GuiApi for State { diff --git a/src/tabline.rs b/src/tabline.rs index 69efe3f..cea0304 100644 --- a/src/tabline.rs +++ b/src/tabline.rs @@ -69,7 +69,7 @@ impl Tabline { fn update_state(&self, nvim: &Rc>, selected: &Tabpage, - tabs: &Vec<(Tabpage, Option<&str>)>) { + tabs: &Vec<(Tabpage, Option)>) { let mut state = self.state.borrow_mut(); if state.nvim.is_none() { @@ -84,7 +84,7 @@ impl Tabline { pub fn update_tabs(&self, nvim: &Rc>, selected: &Tabpage, - tabs: &Vec<(Tabpage, Option<&str>)>) { + tabs: &Vec<(Tabpage, Option)>) { if tabs.len() <= 1 { self.tabs.hide(); return; @@ -113,7 +113,7 @@ impl Tabline { for (idx, tab) in tabs.iter().enumerate() { let tab_child = self.tabs.get_nth_page(Some(idx as u32)); self.tabs - .set_tab_label_text(&tab_child.unwrap(), &tab.1.unwrap_or("??")); + .set_tab_label_text(&tab_child.unwrap(), &tab.1.as_ref().unwrap_or(&"??".to_owned())); if *selected == tab.0 { self.tabs.set_current_page(Some(idx as u32)); diff --git a/src/value.rs b/src/value.rs new file mode 100644 index 0000000..f74e7a1 --- /dev/null +++ b/src/value.rs @@ -0,0 +1,20 @@ +use std::collections::HashMap; +use neovim_lib::Value; + +pub trait ValueMapExt { + fn to_attrs_map(&self) -> Result, String>; +} + +impl ValueMapExt for Vec<(Value, Value)> { + fn to_attrs_map(&self) -> Result, String> { + self.iter() + .map(|p| { + p.0 + .as_str() + .ok_or("Can't convert map key to string".to_owned()) + .map(|key| (key, p.1.clone())) + }) + .collect::, String>>() + + } +}