├── .gitignore ├── .travis.yml ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── fonts ├── Inconsolata-Bold.ttf ├── Inconsolata-Regular.ttf └── OFL.txt ├── screenshot.png └── src ├── channel ├── mod.rs └── source.rs ├── clipboard.rs ├── controller.rs ├── edit_view.rs ├── linecache.rs ├── main.rs ├── main_win.rs ├── prefs_win.rs ├── proto.rs ├── rpc.rs ├── scrollable_drawing_area.rs ├── theme.rs ├── ui ├── find_replace.glade ├── gxi.glade └── prefs_win.glade └── xi_thread.rs /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | *~ 3 | /big.txt 4 | /rls 5 | /TODO 6 | /test 7 | /fonts/.uuid 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: true 2 | language: rust 3 | env: 4 | matrix: 5 | - DIST: "xenial" 6 | RUST_VER: "stable" 7 | - DIST: "xenial" 8 | RUST_VER: "nightly" 9 | - DIST: "bionic" 10 | RUST_VER: "stable" 11 | - DIST: "bionic" 12 | RUST_VER: "nightly" 13 | addons: 14 | apt: 15 | packages: 16 | - libgtk-3-dev 17 | before_install: 18 | - docker run -d --name ubuntu-test -v $(pwd):/travis ubuntu:$DIST tail -f /dev/null 19 | - docker ps 20 | install: 21 | - docker exec -t ubuntu-test bash -c "apt-get update; 22 | apt-get install -y libgtk-3-dev cmake gcc g++;" 23 | script: 24 | - docker exec -t ubuntu-test bash -c "(curl https://sh.rustup.rs -sSf | sh -s -- -y) && 25 | source \$HOME/.cargo/env && 26 | cd /travis && 27 | rustup install $RUST_VER && 28 | rustc --version && 29 | cargo test && 30 | cargo doc && 31 | cargo build" 32 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gxi" 3 | version = "0.2.0" 4 | edition = "2018" 5 | authors = ["Brian Vincent "] 6 | homepage = "https://github.com/bvinc/gxi" 7 | description = "GTK frontend, written in Rust, for the xi editor" 8 | repository = "https://github.com/bvinc/gxi" 9 | keywords = ["gxi", "xi"] 10 | categories = ["editor"] 11 | license = "MIT" 12 | license-file = "LICENSE" 13 | 14 | 15 | [dependencies] 16 | cairo-rs = "0.8" 17 | clap = "2.31" 18 | dirs-next = "1.0" 19 | env_logger = "0.7" 20 | failure = "0.1" 21 | gdk = "0.12" 22 | gio = "0.8" 23 | glib = "0.9" 24 | glib-sys = { version = "0.9", features = ["v2_44"] } 25 | gobject-sys = "0.9" 26 | gtk = { version = "0.8", features = ["v3_18"] } 27 | gtk-sys = { version = "0.9", features = ["v3_18"] } 28 | lazy_static = "1.0" 29 | libc = "0.2" 30 | log = "0.4" 31 | mio = { version = "0.7", features = ["os-util"] } 32 | pango = { version = "0.8", features = ["v1_38"] } 33 | pangocairo = "0.9" 34 | serde = "1.0" 35 | serde_json = "1.0" 36 | serde_derive = "1.0" 37 | servo-fontconfig = "0.5" 38 | xi-core-lib = { git = "https://github.com/google/xi-editor", rev = "65911d9" } 39 | xi-rpc = { git = "https://github.com/google/xi-editor", rev = "65911d9" } 40 | 41 | [profile.release] 42 | lto = true 43 | codegen-units = 1 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Brian Vincent 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gxi 2 | GTK frontend, written in Rust for the [xi editor](https://github.com/google/xi-editor). 3 | 4 | gxi is a work in progress! 5 | 6 | ![screenshot](https://raw.githubusercontent.com/bvinc/gxi/master/screenshot.png) 7 | 8 | ## Instructions 9 | 10 | You need to have the Rust compiler installed. I recommend using [rustup](https://rustup.rs/). 11 | 12 | ### Installing dependencies on Debian/Ubuntu 13 | 14 | ```sh 15 | sudo apt-get install libgtk-3-dev 16 | ``` 17 | 18 | ### Installing dependencies on Redhat 19 | 20 | ```sh 21 | sudo yum install gtk3-devel 22 | ``` 23 | 24 | ### Enabling the syntect syntax highlighting plugin 25 | 26 | Running these commands will put the syntect plugin into your `~/.config/xi/plugins` directory. 27 | 28 | ```sh 29 | git clone https://github.com/google/xi-editor/ 30 | cd xi-editor/rust/syntect-plugin/ 31 | make install 32 | ``` 33 | 34 | ### Running gxi 35 | 36 | ```sh 37 | git clone https://github.com/bvinc/gxi.git 38 | cd gxi 39 | cargo run 40 | ``` 41 | -------------------------------------------------------------------------------- /fonts/Inconsolata-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bvinc/gxi/47d52cb536e6fa9b3c6ca2b6efb49e0efea30054/fonts/Inconsolata-Bold.ttf -------------------------------------------------------------------------------- /fonts/Inconsolata-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bvinc/gxi/47d52cb536e6fa9b3c6ca2b6efb49e0efea30054/fonts/Inconsolata-Regular.ttf -------------------------------------------------------------------------------- /fonts/OFL.txt: -------------------------------------------------------------------------------- 1 | Copyright 2006 The Inconsolata Project Authors 2 | 3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 4 | This license is copied below, and is also available with a FAQ at: 5 | http://scripts.sil.org/OFL 6 | 7 | 8 | ----------------------------------------------------------- 9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 10 | ----------------------------------------------------------- 11 | 12 | PREAMBLE 13 | The goals of the Open Font License (OFL) are to stimulate worldwide 14 | development of collaborative font projects, to support the font creation 15 | efforts of academic and linguistic communities, and to provide a free and 16 | open framework in which fonts may be shared and improved in partnership 17 | with others. 18 | 19 | The OFL allows the licensed fonts to be used, studied, modified and 20 | redistributed freely as long as they are not sold by themselves. The 21 | fonts, including any derivative works, can be bundled, embedded, 22 | redistributed and/or sold with any software provided that any reserved 23 | names are not used by derivative works. The fonts and derivatives, 24 | however, cannot be released under any other type of license. The 25 | requirement for fonts to remain under this license does not apply 26 | to any document created using the fonts or their derivatives. 27 | 28 | DEFINITIONS 29 | "Font Software" refers to the set of files released by the Copyright 30 | Holder(s) under this license and clearly marked as such. This may 31 | include source files, build scripts and documentation. 32 | 33 | "Reserved Font Name" refers to any names specified as such after the 34 | copyright statement(s). 35 | 36 | "Original Version" refers to the collection of Font Software components as 37 | distributed by the Copyright Holder(s). 38 | 39 | "Modified Version" refers to any derivative made by adding to, deleting, 40 | or substituting -- in part or in whole -- any of the components of the 41 | Original Version, by changing formats or by porting the Font Software to a 42 | new environment. 43 | 44 | "Author" refers to any designer, engineer, programmer, technical 45 | writer or other person who contributed to the Font Software. 46 | 47 | PERMISSION & CONDITIONS 48 | Permission is hereby granted, free of charge, to any person obtaining 49 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 50 | redistribute, and sell modified and unmodified copies of the Font 51 | Software, subject to the following conditions: 52 | 53 | 1) Neither the Font Software nor any of its individual components, 54 | in Original or Modified Versions, may be sold by itself. 55 | 56 | 2) Original or Modified Versions of the Font Software may be bundled, 57 | redistributed and/or sold with any software, provided that each copy 58 | contains the above copyright notice and this license. These can be 59 | included either as stand-alone text files, human-readable headers or 60 | in the appropriate machine-readable metadata fields within text or 61 | binary files as long as those fields can be easily viewed by the user. 62 | 63 | 3) No Modified Version of the Font Software may use the Reserved Font 64 | Name(s) unless explicit written permission is granted by the corresponding 65 | Copyright Holder. This restriction only applies to the primary font name as 66 | presented to the users. 67 | 68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 69 | Software shall not be used to promote, endorse or advertise any 70 | Modified Version, except to acknowledge the contribution(s) of the 71 | Copyright Holder(s) and the Author(s) or with their explicit written 72 | permission. 73 | 74 | 5) The Font Software, modified or unmodified, in part or in whole, 75 | must be distributed entirely under this license, and must not be 76 | distributed under any other license. The requirement for fonts to 77 | remain under this license does not apply to any document created 78 | using the Font Software. 79 | 80 | TERMINATION 81 | This license becomes null and void if any of the above conditions are 82 | not met. 83 | 84 | DISCLAIMER 85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 93 | OTHER DEALINGS IN THE FONT SOFTWARE. 94 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bvinc/gxi/47d52cb536e6fa9b3c6ca2b6efb49e0efea30054/screenshot.png -------------------------------------------------------------------------------- /src/channel/mod.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017 Boucher, Antoni 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | #![warn( 23 | missing_docs, 24 | trivial_casts, 25 | trivial_numeric_casts, 26 | unused_extern_crates, 27 | unused_import_braces, 28 | unused_qualifications 29 | )] 30 | 31 | mod source; 32 | 33 | use std::cell::RefCell; 34 | use std::collections::VecDeque; 35 | use std::marker::PhantomData; 36 | use std::rc::Rc; 37 | use std::sync::mpsc::{self, Receiver, SendError}; 38 | 39 | use self::source::{new_source, source_get, SourceFuncs}; 40 | 41 | use glib::{MainContext, Source}; 42 | 43 | /// A lock is used to temporarily stop emitting messages. 44 | #[must_use] 45 | pub struct Lock { 46 | stream: Rc>>, 47 | } 48 | 49 | impl Drop for Lock { 50 | fn drop(&mut self) { 51 | self.stream.borrow_mut().locked = false; 52 | } 53 | } 54 | 55 | struct ChannelData { 56 | callback: Box, 57 | peeked_value: Option, 58 | receiver: Receiver, 59 | } 60 | 61 | /// A wrapper over a `std::sync::mpsc::Sender` to wakeup the glib event loop when sending a 62 | /// message. 63 | pub struct Sender { 64 | sender: mpsc::Sender, 65 | } 66 | 67 | impl Clone for Sender { 68 | fn clone(&self) -> Self { 69 | Self { 70 | sender: self.sender.clone(), 71 | } 72 | } 73 | } 74 | 75 | impl Sender { 76 | /// Send a message and wakeup the event loop. 77 | pub fn send(&self, msg: MSG) -> Result<(), SendError> { 78 | let result = self.sender.send(msg); 79 | let context = MainContext::default(); 80 | context.wakeup(); 81 | result 82 | } 83 | } 84 | 85 | /// A channel to send a message to a relm widget from another thread. 86 | pub struct Channel { 87 | _source: Source, 88 | _phantom: PhantomData, 89 | } 90 | 91 | impl Channel { 92 | /// Create a new channel with a callback that will be called when a message is received. 93 | pub fn new(callback: CALLBACK) -> (Self, Sender) { 94 | let (sender, receiver) = mpsc::channel(); 95 | let source = new_source(RefCell::new(ChannelData { 96 | callback: Box::new(callback), 97 | peeked_value: None, 98 | receiver, 99 | })); 100 | let main_context = MainContext::default(); 101 | source.attach(Some(&main_context)); 102 | ( 103 | Self { 104 | _source: source, 105 | _phantom: PhantomData, 106 | }, 107 | Sender { sender }, 108 | ) 109 | } 110 | } 111 | 112 | impl SourceFuncs for RefCell> { 113 | fn dispatch(&self) -> bool { 114 | // TODO: show errors. 115 | let msg = self 116 | .borrow_mut() 117 | .peeked_value 118 | .take() 119 | .or_else(|| self.borrow().receiver.try_recv().ok()); 120 | if let Some(msg) = msg { 121 | let callback = &mut self.borrow_mut().callback; 122 | callback(msg); 123 | } 124 | true 125 | } 126 | 127 | fn prepare(&self) -> (bool, Option) { 128 | if self.borrow().peeked_value.is_some() { 129 | return (true, None); 130 | } 131 | let peek_val = self.borrow().receiver.try_recv().ok(); 132 | self.borrow_mut().peeked_value = peek_val; 133 | (self.borrow().peeked_value.is_some(), None) 134 | } 135 | } 136 | 137 | struct _EventStream { 138 | events: VecDeque, 139 | locked: bool, 140 | observers: Vec>, 141 | } 142 | 143 | impl SourceFuncs for SourceData { 144 | fn dispatch(&self) -> bool { 145 | let event = self.stream.borrow_mut().events.pop_front(); 146 | if let (Some(event), Some(callback)) = (event, self.callback.borrow_mut().as_mut()) { 147 | callback(event); 148 | } 149 | true 150 | } 151 | 152 | fn prepare(&self) -> (bool, Option) { 153 | (!self.stream.borrow().events.is_empty(), None) 154 | } 155 | } 156 | 157 | struct SourceData { 158 | callback: Rc>>>, 159 | stream: Rc>>, 160 | } 161 | 162 | /// A stream of messages to be used for widget/signal communication and inter-widget communication. 163 | /// EventStream cannot be send to another thread. Use a `Channel` `Sender` instead. 164 | pub struct EventStream { 165 | source: Source, 166 | _phantom: PhantomData<*mut MSG>, 167 | } 168 | 169 | impl Clone for EventStream { 170 | fn clone(&self) -> Self { 171 | EventStream { 172 | source: self.source.clone(), 173 | _phantom: PhantomData, 174 | } 175 | } 176 | } 177 | 178 | impl EventStream { 179 | fn get_callback(&self) -> Rc>>> { 180 | source_get::>(&self.source).callback.clone() 181 | } 182 | 183 | fn get_stream(&self) -> Rc>> { 184 | source_get::>(&self.source).stream.clone() 185 | } 186 | } 187 | 188 | impl EventStream { 189 | /// Create a new event stream. 190 | pub fn new() -> Self { 191 | let event_stream: _EventStream = _EventStream { 192 | events: VecDeque::new(), 193 | locked: false, 194 | observers: vec![], 195 | }; 196 | let source = new_source(SourceData { 197 | callback: Rc::new(RefCell::new(None)), 198 | stream: Rc::new(RefCell::new(event_stream)), 199 | }); 200 | let main_context = MainContext::default(); 201 | source.attach(Some(&main_context)); 202 | EventStream { 203 | source, 204 | _phantom: PhantomData, 205 | } 206 | } 207 | 208 | /// Close the event stream, i.e. stop processing messages. 209 | pub fn close(&self) { 210 | self.source.destroy(); 211 | } 212 | 213 | /// Send the `event` message to the stream and the observers. 214 | pub fn emit(&self, event: MSG) { 215 | let stream = self.get_stream(); 216 | if !stream.borrow().locked { 217 | let len = stream.borrow().observers.len(); 218 | for i in 0..len { 219 | let observer = stream.borrow().observers[i].clone(); 220 | observer(&event); 221 | } 222 | 223 | stream.borrow_mut().events.push_back(event); 224 | } 225 | } 226 | 227 | /// Lock the stream (don't emit message) until the `Lock` goes out of scope. 228 | pub fn lock(&self) -> Lock { 229 | let stream = self.get_stream(); 230 | stream.borrow_mut().locked = true; 231 | Lock { 232 | stream: self.get_stream().clone(), 233 | } 234 | } 235 | 236 | /// Add an observer to the event stream. 237 | /// This callback will be called every time a message is emmited. 238 | pub fn observe(&self, callback: CALLBACK) { 239 | let stream = self.get_stream(); 240 | stream.borrow_mut().observers.push(Rc::new(callback)); 241 | } 242 | 243 | /// Add a callback to the event stream. 244 | /// This is the main callback and received a owned version of the message, in contrast to 245 | /// observe(). 246 | pub fn set_callback(&self, callback: CALLBACK) { 247 | let source_callback = self.get_callback(); 248 | *source_callback.borrow_mut() = Some(Box::new(callback)); 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /src/channel/source.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 Boucher, Antoni 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | use std::mem; 23 | use std::os::raw::c_int; 24 | use std::ptr; 25 | 26 | use glib::translate::{from_glib_none, ToGlibPtr}; 27 | use glib::Source; 28 | use glib_sys::{g_source_new, GSource, GSourceFunc, GSourceFuncs}; 29 | use libc; 30 | 31 | pub trait SourceFuncs { 32 | fn check(&self) -> bool { 33 | false 34 | } 35 | 36 | fn dispatch(&self) -> bool; 37 | fn prepare(&self) -> (bool, Option); 38 | } 39 | 40 | struct SourceData { 41 | _source: GSource, 42 | funcs: Box, 43 | data: T, 44 | } 45 | 46 | pub fn new_source(data: T) -> Source { 47 | unsafe { 48 | let mut funcs: GSourceFuncs = mem::zeroed(); 49 | funcs.prepare = Some(prepare::); 50 | funcs.check = Some(check::); 51 | funcs.dispatch = Some(dispatch::); 52 | funcs.finalize = Some(finalize::); 53 | let mut funcs = Box::new(funcs); 54 | let source = g_source_new(&mut *funcs, mem::size_of::>() as u32); 55 | ptr::write(&mut (*(source as *mut SourceData)).data, data); 56 | ptr::write(&mut (*(source as *mut SourceData)).funcs, funcs); 57 | from_glib_none(source) 58 | } 59 | } 60 | 61 | pub fn source_get(source: &Source) -> &T { 62 | unsafe { &(*(source.to_glib_none().0 as *const SourceData)).data } 63 | } 64 | 65 | unsafe extern "C" fn check(source: *mut GSource) -> c_int { 66 | let object = source as *mut SourceData; 67 | bool_to_int((*object).data.check()) 68 | } 69 | 70 | unsafe extern "C" fn dispatch( 71 | source: *mut GSource, 72 | _callback: GSourceFunc, 73 | _user_data: *mut libc::c_void, 74 | ) -> c_int { 75 | let object = source as *mut SourceData; 76 | bool_to_int((*object).data.dispatch()) 77 | } 78 | 79 | unsafe extern "C" fn finalize(source: *mut GSource) { 80 | // TODO: needs a bomb to abort on panic 81 | let source = source as *mut SourceData; 82 | ptr::read(&(*source).funcs); 83 | ptr::read(&(*source).data); 84 | } 85 | 86 | extern "C" fn prepare(source: *mut GSource, timeout: *mut c_int) -> c_int { 87 | let object = source as *mut SourceData; 88 | let (result, source_timeout) = unsafe { (*object).data.prepare() }; 89 | if let Some(source_timeout) = source_timeout { 90 | unsafe { 91 | *timeout = source_timeout as i32; 92 | } 93 | } 94 | bool_to_int(result) 95 | } 96 | 97 | fn bool_to_int(boolean: bool) -> c_int { 98 | if boolean { 99 | 1 100 | } else { 101 | 0 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/clipboard.rs: -------------------------------------------------------------------------------- 1 | use glib::translate::*; 2 | use glib_sys::gpointer; 3 | use gtk::Clipboard; 4 | use gtk_sys as ffi; 5 | use libc::{self, c_char}; 6 | use std::mem::transmute; 7 | 8 | pub trait ClipboardRequest { 9 | fn request_text(&self, callback: F); 10 | } 11 | 12 | impl ClipboardRequest for Clipboard { 13 | fn request_text(&self, callback: F) { 14 | unsafe { 15 | let trampoline = trampoline_request_text as *mut libc::c_void; 16 | ffi::gtk_clipboard_request_text( 17 | self.to_glib_none().0, 18 | Some(transmute(trampoline)), 19 | into_raw_request_text(callback), 20 | ); 21 | } 22 | } 23 | } 24 | 25 | fn into_raw_request_text(func: F) -> gpointer { 26 | let func: Box> = Box::new(Box::new(func)); 27 | Box::into_raw(func) as gpointer 28 | } 29 | 30 | unsafe extern "C" fn trampoline_request_text( 31 | clipboard: *mut ffi::GtkClipboard, 32 | text: *const c_char, 33 | data: gpointer, 34 | ) { 35 | let f: &&(dyn Fn(&Clipboard, String) + 'static) = transmute(data); 36 | f(&from_glib_borrow(clipboard), from_glib_none(text)); 37 | } 38 | -------------------------------------------------------------------------------- /src/controller.rs: -------------------------------------------------------------------------------- 1 | use crate::channel::{Channel, Sender}; 2 | use crate::main_win::MainWin; 3 | use crate::rpc::Core; 4 | use log::*; 5 | use serde_json::{json, Value}; 6 | use std::cell::RefCell; 7 | use std::rc::Rc; 8 | 9 | #[derive(Clone, Debug)] 10 | pub enum CoreMsg { 11 | Notification { 12 | method: String, 13 | params: Value, 14 | }, 15 | NewViewReply { 16 | file_name: Option, 17 | value: Value, 18 | }, 19 | } 20 | 21 | pub type ControllerRef = Rc>; 22 | 23 | #[derive(Default)] 24 | pub struct Controller { 25 | core: Option, 26 | channel: Option>, 27 | sender: Option>, 28 | main_win: Option>, 29 | } 30 | 31 | impl Controller { 32 | pub fn new() -> ControllerRef { 33 | Rc::new(RefCell::new(Controller { 34 | ..Default::default() 35 | })) 36 | } 37 | 38 | pub fn core(&self) -> &Core { 39 | self.core.as_ref().expect("no core") 40 | } 41 | 42 | pub fn set_core(&mut self, core: Core) { 43 | self.core = Some(core); 44 | } 45 | 46 | pub fn set_channel(&mut self, channel: Channel) { 47 | self.channel = Some(channel); 48 | } 49 | 50 | pub fn sender(&self) -> &Sender { 51 | self.sender.as_ref().expect("no sender") 52 | } 53 | pub fn set_sender(&mut self, sender: Sender) { 54 | self.sender = Some(sender); 55 | } 56 | 57 | fn main_win(&self) -> &RefCell { 58 | self.main_win.as_ref().expect("no main win") 59 | } 60 | pub fn set_main_win(&mut self, main_win: MainWin) { 61 | self.main_win = Some(RefCell::new(main_win)); 62 | } 63 | 64 | pub fn handle_msg(&self, msg: CoreMsg) { 65 | match msg { 66 | CoreMsg::NewViewReply { file_name, value } => self 67 | .main_win() 68 | .borrow_mut() 69 | .new_view_response(file_name, &value), 70 | CoreMsg::Notification { method, params } => { 71 | match method.as_ref() { 72 | "available_themes" => self.main_win().borrow_mut().available_themes(¶ms), 73 | "available_plugins" => self.main_win().borrow_mut().available_plugins(¶ms), 74 | "config_changed" => self.main_win().borrow_mut().config_changed(¶ms), 75 | "def_style" => self.main_win().borrow_mut().def_style(¶ms), 76 | "find_status" => self.main_win().borrow_mut().find_status(¶ms), 77 | "update" => self.main_win().borrow_mut().update(¶ms), 78 | "scroll_to" => self.main_win().borrow_mut().scroll_to(¶ms), 79 | "theme_changed" => self.main_win().borrow_mut().theme_changed(¶ms), 80 | _ => { 81 | error!("!!! UNHANDLED NOTIFICATION: {}", method); 82 | } 83 | }; 84 | } 85 | }; 86 | } 87 | 88 | pub fn req_new_view(&self, file_name: Option<&str>) { 89 | let mut params = json!({}); 90 | if let Some(file_name) = file_name { 91 | params["file_path"] = json!(file_name); 92 | } else { 93 | params["file_path"] = Value::Null; 94 | } 95 | 96 | let sender2 = self.sender().clone(); 97 | let file_name2 = file_name.map(|s| s.to_string()); 98 | self.core().send_request("new_view", ¶ms, move |value| { 99 | let value = value.clone(); 100 | sender2 101 | .send(CoreMsg::NewViewReply { 102 | file_name: file_name2, 103 | value, 104 | }) 105 | .expect("send failed"); 106 | }); 107 | } 108 | pub fn set_theme(&self, theme_name: &str) { 109 | self.core() 110 | .send_notification("set_theme", &json!({ "theme_name": theme_name })); 111 | } 112 | 113 | pub fn set_auto_indent(&self, auto_indent: bool) { 114 | self.core() 115 | .modify_user_config(&json!("general"), &json!({ "auto_indent": auto_indent })); 116 | } 117 | 118 | pub fn save(&self, view_id: &str, file_path: &str) { 119 | self.core().save(view_id, file_path) 120 | } 121 | pub fn close_view(&self, view_id: &str) { 122 | self.main_win().borrow_mut().close_view(view_id); 123 | self.core().close_view(view_id) 124 | } 125 | 126 | pub fn handle_open_button(&self) { 127 | self.main_win().borrow_mut().handle_open_button(); 128 | } 129 | pub fn prefs(&self) { 130 | self.main_win().borrow_mut().prefs(); 131 | } 132 | pub fn find(&self) { 133 | self.main_win().borrow_mut().find(); 134 | } 135 | pub fn handle_save_button(&self) { 136 | self.main_win().borrow_mut().handle_save_button(); 137 | } 138 | pub fn current_save_as(&self) { 139 | self.main_win().borrow_mut().current_save_as(); 140 | } 141 | pub fn close(&self) { 142 | self.main_win().borrow_mut().close(); 143 | } 144 | pub fn close_all(&self) { 145 | self.main_win().borrow_mut().close_all(); 146 | } 147 | pub fn quit(&self) { 148 | self.main_win().borrow_mut().quit(); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/edit_view.rs: -------------------------------------------------------------------------------- 1 | use crate::controller::ControllerRef; 2 | use crate::linecache::{Line, LineCache}; 3 | use crate::main_win::MainState; 4 | use crate::rpc::{self}; 5 | use crate::scrollable_drawing_area::ScrollableDrawingArea; 6 | use crate::theme::set_source_color; 7 | use cairo::Context; 8 | use gdk::enums::key; 9 | use gdk::*; 10 | use glib::clone; 11 | use gtk::prelude::*; 12 | use gtk::{self, *}; 13 | use log::*; 14 | use pango::{self, *}; 15 | use pangocairo::functions::*; 16 | use serde_json::Value; 17 | use std::cell::RefCell; 18 | use std::cmp::{max, min}; 19 | use std::ops::Range; 20 | use std::rc::Rc; 21 | use std::u32; 22 | 23 | pub struct EditView { 24 | controller: ControllerRef, 25 | main_state: Rc>, 26 | pub view_id: String, 27 | pub file_name: Option, 28 | pub pristine: bool, 29 | pub line_da: ScrollableDrawingArea, 30 | pub da: ScrollableDrawingArea, 31 | pub root_widget: gtk::Box, 32 | pub tab_widget: gtk::Box, 33 | pub label: Label, 34 | pub close_button: Button, 35 | search_bar: SearchBar, 36 | search_entry: SearchEntry, 37 | replace_expander: Expander, 38 | replace_revealer: Revealer, 39 | replace_entry: Entry, 40 | find_status_label: Label, 41 | hadj: Adjustment, 42 | vadj: Adjustment, 43 | line_cache: LineCache, 44 | font_height: f64, 45 | font_width: f64, 46 | font_ascent: f64, 47 | font_descent: f64, 48 | font_desc: FontDescription, 49 | visible_lines: Range, 50 | } 51 | 52 | impl EditView { 53 | pub fn new( 54 | main_state: Rc>, 55 | controller: ControllerRef, 56 | file_name: Option, 57 | view_id: &str, 58 | ) -> Rc> { 59 | // let da = DrawingArea::new(); 60 | let da = ScrollableDrawingArea::new(); 61 | let line_da = ScrollableDrawingArea::new(); 62 | line_da.set_size_request(100, 100); 63 | let sw_hadj: Option<&Adjustment> = None; 64 | let sw_vadj: Option<&Adjustment> = None; 65 | let scrolled_window = ScrolledWindow::new(sw_hadj, sw_vadj); 66 | scrolled_window.add(&da); 67 | 68 | let hadj = Adjustment::new(0.0, 0.0, 0.0, 0.0, 0.0, 0.0); 69 | let vadj = Adjustment::new(0.0, 0.0, 0.0, 0.0, 0.0, 0.0); 70 | vadj.set_step_increment(1.0); 71 | 72 | scrolled_window.set_hadjustment(Some(&hadj)); 73 | scrolled_window.set_vadjustment(Some(&vadj)); 74 | scrolled_window.set_kinetic_scrolling(true); 75 | 76 | da.set_events( 77 | EventMask::BUTTON_PRESS_MASK 78 | | EventMask::BUTTON_RELEASE_MASK 79 | | EventMask::BUTTON_MOTION_MASK 80 | | EventMask::SCROLL_MASK 81 | | EventMask::SMOOTH_SCROLL_MASK, 82 | ); 83 | debug!("events={:?}", da.get_events()); 84 | da.set_can_focus(true); 85 | 86 | let find_rep_src = include_str!("ui/find_replace.glade"); 87 | let find_rep_builder = Builder::new_from_string(find_rep_src); 88 | let search_bar: SearchBar = find_rep_builder.get_object("search_bar").unwrap(); 89 | let replace_expander: Expander = find_rep_builder.get_object("replace_expander").unwrap(); 90 | let replace_revealer: Revealer = find_rep_builder.get_object("replace_revealer").unwrap(); 91 | let replace_entry: Entry = find_rep_builder.get_object("replace_entry").unwrap(); 92 | let replace_button: Button = find_rep_builder.get_object("replace_button").unwrap(); 93 | let replace_all_button: Button = find_rep_builder.get_object("replace_all_button").unwrap(); 94 | let find_status_label: Label = find_rep_builder.get_object("find_status_label").unwrap(); 95 | 96 | // let overlay: Overlay = frame_builder.get_object("overlay").unwrap(); 97 | // let search_revealer: Revealer = frame_builder.get_object("revealer").unwrap(); 98 | // let frame: Frame = frame_builder.get_object("frame").unwrap(); 99 | let search_entry: SearchEntry = find_rep_builder.get_object("search_entry").unwrap(); 100 | let go_down_button: Button = find_rep_builder.get_object("go_down_button").unwrap(); 101 | let go_up_button: Button = find_rep_builder.get_object("go_up_button").unwrap(); 102 | 103 | // let style_context = frame.get_style_context().unwrap(); 104 | // style_context.add_provider(&css_provider, 1); 105 | 106 | let line_hbox = Box::new(Orientation::Horizontal, 0); 107 | line_hbox.pack_start(&line_da, false, false, 0); 108 | line_hbox.pack_start(&scrolled_window, true, true, 0); 109 | 110 | let main_vbox = Box::new(Orientation::Vertical, 0); 111 | main_vbox.pack_start(&search_bar, false, false, 0); 112 | main_vbox.pack_start(&line_hbox, true, true, 0); 113 | 114 | main_vbox.show_all(); 115 | 116 | // Make the widgets for the tab 117 | let tab_hbox = gtk::Box::new(Orientation::Horizontal, 5); 118 | let label = Label::new(Some("")); 119 | tab_hbox.add(&label); 120 | let close_button = Button::new_from_icon_name(Some("window-close"), IconSize::SmallToolbar); 121 | tab_hbox.add(&close_button); 122 | tab_hbox.show_all(); 123 | 124 | use ::fontconfig::fontconfig; 125 | use std::ffi::CString; 126 | unsafe { 127 | let fonts_dir = CString::new("fonts").unwrap(); 128 | let ret = fontconfig::FcConfigAppFontAddDir( 129 | fontconfig::FcConfigGetCurrent(), 130 | fonts_dir.as_ptr() as *const u8, 131 | ); 132 | debug!("fc ret = {}", ret); 133 | } 134 | 135 | let pango_ctx = da.get_pango_context().expect("failed to get pango ctx"); 136 | for family in pango_ctx.list_families() { 137 | if !family.is_monospace() { 138 | continue; 139 | } 140 | debug!( 141 | "font family {:?} monospace: {}", 142 | family.get_name(), 143 | family.is_monospace() 144 | ); 145 | } 146 | let font_desc = FontDescription::from_string("Inconsolata 14"); 147 | pango_ctx.set_font_description(&font_desc); 148 | let language = pango_ctx 149 | .get_language() 150 | .expect("failed to get pango language"); 151 | let fontset = pango_ctx 152 | .load_fontset(&font_desc, &language) 153 | .expect("failed to load font set"); 154 | let metrics = fontset.get_metrics().expect("failed to load font metrics"); 155 | 156 | // cr.select_font_face("Inconsolata", ::cairo::enums::FontSlant::Normal, ::cairo::enums::FontWeight::Normal); 157 | // cr.set_font_size(16.0); 158 | // let font_extents = cr.font_extents(); 159 | 160 | let layout = pango::Layout::new(&pango_ctx); 161 | layout.set_text("a"); 162 | let (_, log_extents) = layout.get_extents(); 163 | debug!("size: {:?}", log_extents); 164 | 165 | let font_height = f64::from(log_extents.height) / f64::from(pango::SCALE); 166 | let font_width = f64::from(log_extents.width) / f64::from(pango::SCALE); 167 | let font_ascent = f64::from(metrics.get_ascent()) / f64::from(pango::SCALE); 168 | let font_descent = f64::from(metrics.get_descent()) / f64::from(pango::SCALE); 169 | 170 | debug!( 171 | "font metrics: {} {} {} {}", 172 | font_width, font_height, font_ascent, font_descent 173 | ); 174 | 175 | let edit_view = Rc::new(RefCell::new(EditView { 176 | controller: controller.clone(), 177 | main_state: main_state.clone(), 178 | file_name, 179 | pristine: true, 180 | view_id: view_id.to_string(), 181 | line_da: line_da.clone(), 182 | da: da.clone(), 183 | root_widget: main_vbox.clone(), 184 | tab_widget: tab_hbox.clone(), 185 | label: label.clone(), 186 | close_button: close_button.clone(), 187 | search_bar: search_bar.clone(), 188 | search_entry: search_entry.clone(), 189 | replace_expander: replace_expander.clone(), 190 | replace_revealer: replace_revealer.clone(), 191 | replace_entry: replace_entry.clone(), 192 | find_status_label: find_status_label.clone(), 193 | hadj: hadj.clone(), 194 | vadj: vadj.clone(), 195 | line_cache: LineCache::new(), 196 | font_height, 197 | font_width, 198 | font_ascent, 199 | font_descent, 200 | font_desc, 201 | visible_lines: 0..1, 202 | })); 203 | 204 | edit_view.borrow_mut().update_title(); 205 | 206 | line_da.connect_draw(clone!(@strong edit_view => move |_,ctx| { 207 | edit_view.borrow_mut().handle_line_draw(&ctx) 208 | })); 209 | 210 | da.connect_button_press_event(clone!(@strong edit_view => move |_,eb| { 211 | edit_view.borrow().handle_button_press(eb) 212 | })); 213 | 214 | da.connect_draw(clone!(@strong edit_view => move |_,ctx| { 215 | edit_view.borrow_mut().handle_draw(&ctx) 216 | })); 217 | 218 | da.connect_key_press_event(clone!(@strong edit_view => move |_, ek| { 219 | edit_view.borrow_mut().handle_key_press_event(ek) 220 | })); 221 | 222 | da.connect_motion_notify_event(clone!(@strong edit_view => move |_,em| { 223 | edit_view.borrow_mut().handle_drag(em) 224 | })); 225 | 226 | da.connect_realize(|w| { 227 | // Set the text cursor 228 | if let Some(disp) = DisplayManager::get().get_default_display() { 229 | let cur = Cursor::new_for_display(&disp, CursorType::Xterm); 230 | if let Some(win) = w.get_window() { 231 | win.set_cursor(Some(&cur)) 232 | } 233 | } 234 | w.grab_focus(); 235 | }); 236 | 237 | da.connect_scroll_event(clone!(@strong edit_view => move |_,es| { 238 | edit_view.borrow_mut().handle_scroll(es) 239 | })); 240 | 241 | da.connect_size_allocate(clone!(@strong edit_view => move |_,alloc| { 242 | debug!("Size changed to w={} h={}", alloc.width, alloc.height); 243 | edit_view.borrow_mut().da_size_allocate(alloc.width, alloc.height); 244 | })); 245 | 246 | search_entry.connect_search_changed(clone!(@strong edit_view => move |w| { 247 | edit_view.borrow_mut().search_changed(w.get_text().map(|gs| gs.as_str().to_owned())); 248 | })); 249 | 250 | search_entry.connect_activate(clone!(@strong edit_view => move |w| { 251 | edit_view.borrow_mut().find_next(); 252 | })); 253 | 254 | search_entry.connect_stop_search(clone!(@strong edit_view => move |w| { 255 | edit_view.borrow().stop_search(); 256 | })); 257 | 258 | replace_expander.connect_property_expanded_notify( 259 | clone!(@strong replace_revealer => move|w| { 260 | if w.get_expanded() { 261 | replace_revealer.set_reveal_child(true); 262 | } else { 263 | replace_revealer.set_reveal_child(false); 264 | } 265 | }), 266 | ); 267 | 268 | replace_button.connect_clicked(clone!(@strong edit_view => move |w| { 269 | edit_view.borrow().replace(); 270 | })); 271 | 272 | replace_all_button.connect_clicked(clone!(@strong edit_view => move |w| { 273 | edit_view.borrow().replace_all(); 274 | })); 275 | 276 | go_down_button.connect_clicked(clone!(@strong edit_view => move |_| { 277 | edit_view.borrow_mut().find_next(); 278 | })); 279 | 280 | go_up_button.connect_clicked(clone!(@strong edit_view => move |_| { 281 | edit_view.borrow_mut().find_prev(); 282 | })); 283 | 284 | vadj.connect_value_changed(clone!(@strong edit_view => move |_| { 285 | edit_view.borrow_mut().update_visible_scroll_region(); 286 | })); 287 | 288 | edit_view 289 | } 290 | } 291 | 292 | fn convert_gtk_modifier(mt: ModifierType) -> u32 { 293 | let mut ret = 0; 294 | if mt.contains(ModifierType::SHIFT_MASK) { 295 | ret |= rpc::XI_SHIFT_KEY_MASK; 296 | } 297 | if mt.contains(ModifierType::CONTROL_MASK) { 298 | ret |= rpc::XI_CONTROL_KEY_MASK; 299 | } 300 | if mt.contains(ModifierType::MOD1_MASK) { 301 | ret |= rpc::XI_ALT_KEY_MASK; 302 | } 303 | ret 304 | } 305 | 306 | impl EditView { 307 | pub fn set_file(&mut self, file_name: &str) { 308 | self.file_name = Some(file_name.to_string()); 309 | self.update_title(); 310 | } 311 | 312 | fn update_title(&self) { 313 | let title = match self.file_name { 314 | Some(ref f) => f 315 | .split(::std::path::MAIN_SEPARATOR) 316 | .last() 317 | .unwrap_or("Untitled") 318 | .to_string(), 319 | None => "Untitled".to_string(), 320 | }; 321 | 322 | let mut full_title = String::new(); 323 | if !self.pristine { 324 | full_title.push('*'); 325 | } 326 | full_title.push_str(&title); 327 | 328 | trace!("setting title to {}", full_title); 329 | self.label.set_text(&full_title); 330 | } 331 | 332 | pub fn config_changed(&mut self, changes: &Value) { 333 | if let Some(map) = changes.as_object() { 334 | for (name, value) in map { 335 | match name.as_ref() { 336 | "font_size" => { 337 | if let Some(font_size) = value.as_u64() { 338 | self.font_desc.set_size(font_size as i32 * pango::SCALE); 339 | } 340 | } 341 | "font_face" => { 342 | if let Some(font_face) = value.as_str() { 343 | if font_face == "InconsolataGo" { 344 | // TODO This shouldn't be necessary, but the only font I've found 345 | // to bundle is "Inconsolata" 346 | self.font_desc.set_family("Inconsolata"); 347 | } else { 348 | self.font_desc.set_family(font_face); 349 | } 350 | } 351 | } 352 | _ => { 353 | error!("unhandled config option {}", name); 354 | } 355 | } 356 | } 357 | } 358 | } 359 | 360 | pub fn update(edit_view: &Rc>, params: &Value) { 361 | let update = ¶ms["update"]; 362 | let (text_width, text_height, vadj, hadj) = { 363 | let mut ev = edit_view.borrow_mut(); 364 | ev.line_cache.apply_update(update); 365 | 366 | if let Some(pristine) = update["pristine"].as_bool() { 367 | if ev.pristine != pristine { 368 | ev.pristine = pristine; 369 | ev.update_title(); 370 | } 371 | } 372 | 373 | ev.line_da.queue_draw(); 374 | ev.da.queue_draw(); 375 | 376 | let (text_width, text_height) = ev.get_text_size(); 377 | let vadj = ev.vadj.clone(); 378 | let hadj = ev.hadj.clone(); 379 | 380 | (text_width, text_height, vadj, hadj) 381 | }; 382 | 383 | // update scrollbars to the new text width and height 384 | vadj.set_lower(0f64); 385 | vadj.set_upper(text_height as f64); 386 | if vadj.get_value() + vadj.get_page_size() > vadj.get_upper() { 387 | vadj.set_value(vadj.get_upper() - vadj.get_page_size()) 388 | } 389 | 390 | // hadj.set_lower(0f64); 391 | // hadj.set_upper(text_width as f64); 392 | // if hadj.get_value() + hadj.get_page_size() > hadj.get_upper() { 393 | // hadj.set_value(hadj.get_upper() - hadj.get_page_size()) 394 | // } 395 | } 396 | 397 | pub fn da_px_to_cell(&self, main_state: &MainState, x: f64, y: f64) -> (u64, u64) { 398 | // let first_line = (vadj.get_value() / font_extents.height) as usize; 399 | let x = x + self.hadj.get_value(); 400 | let y = y + self.vadj.get_value(); 401 | 402 | let mut y = y - self.font_descent; 403 | if y < 0.0 { 404 | y = 0.0; 405 | } 406 | let line_num = (y / self.font_height) as u64; 407 | let index = if let Some(line) = self.line_cache.get_line(line_num) { 408 | let pango_ctx = self 409 | .da 410 | .get_pango_context() 411 | .expect("failed to get pango ctx"); 412 | 413 | let layout = self.create_layout_for_line(&pango_ctx, &main_state, line); 414 | let (_, index, trailing) = layout.xy_to_index(x as i32 * pango::SCALE, 0); 415 | index + trailing 416 | } else { 417 | 0 418 | }; 419 | (index as u64, (y / self.font_height) as u64) 420 | } 421 | 422 | fn da_size_allocate(&mut self, da_width: i32, da_height: i32) { 423 | debug!("DA SIZE ALLOCATE"); 424 | let vadj = self.vadj.clone(); 425 | vadj.set_page_size(f64::from(da_height)); 426 | let hadj = self.hadj.clone(); 427 | hadj.set_page_size(f64::from(da_width)); 428 | 429 | self.update_visible_scroll_region(); 430 | } 431 | 432 | /// Inform core that the visible scroll region has changed 433 | fn update_visible_scroll_region(&mut self) { 434 | let main_state = self.main_state.borrow(); 435 | let da_height = self.da.get_allocated_height(); 436 | let (_, first_line) = self.da_px_to_cell(&main_state, 0.0, 0.0); 437 | let (_, last_line) = self.da_px_to_cell(&main_state, 0.0, f64::from(da_height)); 438 | let last_line = last_line + 1; 439 | let visible_lines = first_line..last_line; 440 | if visible_lines != self.visible_lines { 441 | self.visible_lines = visible_lines; 442 | self.controller 443 | .borrow() 444 | .core() 445 | .scroll(&self.view_id, first_line, last_line); 446 | } 447 | } 448 | 449 | fn get_text_size(&self) -> (f64, f64) { 450 | let da_width = f64::from(self.da.get_allocated_width()); 451 | let da_height = f64::from(self.da.get_allocated_height()); 452 | let num_lines = self.line_cache.height(); 453 | 454 | let all_text_height = num_lines as f64 * self.font_height + self.font_descent; 455 | let height = if da_height > all_text_height { 456 | da_height 457 | } else { 458 | all_text_height 459 | }; 460 | 461 | let all_text_width = self.line_cache.width() as f64 * self.font_width; 462 | let width = if da_width > all_text_width { 463 | da_width 464 | } else { 465 | all_text_width 466 | }; 467 | (width, height) 468 | } 469 | 470 | pub fn handle_line_draw(&mut self, cr: &Context) -> Inhibit { 471 | // let foreground = self.main_state.borrow().theme.foreground; 472 | let theme = &self.main_state.borrow().theme; 473 | 474 | let da_width = self.line_da.get_allocated_width(); 475 | let da_height = self.line_da.get_allocated_height(); 476 | 477 | let num_lines = self.line_cache.height(); 478 | 479 | let vadj = self.vadj.clone(); 480 | // let hadj = self.hadj.clone(); 481 | trace!("drawing. vadj={}, {}", vadj.get_value(), vadj.get_upper()); 482 | 483 | let first_line = (vadj.get_value() / self.font_height) as u64; 484 | let last_line = ((vadj.get_value() + f64::from(da_height)) / self.font_height) as u64 + 1; 485 | let last_line = min(last_line, num_lines); 486 | 487 | // Find missing lines 488 | let mut found_missing = false; 489 | for i in first_line..last_line { 490 | if self.line_cache.get_line(i).is_none() { 491 | debug!("missing line {}", i); 492 | found_missing = true; 493 | } 494 | } 495 | 496 | // We've already missed our chance to draw these lines, but we need to request them for the 497 | // next frame. This needs to be improved to prevent flashing. 498 | if found_missing { 499 | debug!( 500 | "didn't have some lines, requesting, lines {}-{}", 501 | first_line, last_line 502 | ); 503 | self.controller.borrow().core().request_lines( 504 | &self.view_id, 505 | first_line as u64, 506 | last_line as u64, 507 | ); 508 | } 509 | 510 | let pango_ctx = self.da.get_pango_context().unwrap(); 511 | pango_ctx.set_font_description(&self.font_desc); 512 | 513 | // Calculate ordinal or max line length 514 | let padding: usize = format!("{}", num_lines.saturating_sub(1)).len(); 515 | 516 | let main_state = self.main_state.borrow(); 517 | 518 | // Just get the gutter size 519 | let mut gutter_size = 0.0; 520 | let pango_ctx = self 521 | .da 522 | .get_pango_context() 523 | .expect("failed to get pango ctx"); 524 | let linecount_layout = 525 | self.create_layout_for_linecount(&pango_ctx, &main_state, 0, padding); 526 | update_layout(cr, &linecount_layout); 527 | // show_layout(cr, &linecount_layout); 528 | 529 | let linecount_offset = (linecount_layout.get_extents().1.width / pango::SCALE) as f64; 530 | if linecount_offset > gutter_size { 531 | gutter_size = linecount_offset; 532 | } 533 | let gutter_size = gutter_size as i32; 534 | 535 | self.line_da.set_size_request(gutter_size, 0); 536 | 537 | // Draw the gutter background 538 | set_source_color(cr, theme.gutter); 539 | cr.rectangle(0.0, 0.0, f64::from(da_width), f64::from(da_height)); 540 | cr.fill(); 541 | 542 | for i in first_line..last_line { 543 | // Keep track of the starting x position 544 | if let Some(_) = self.line_cache.get_line(i) { 545 | cr.move_to(0.0, self.font_height * (i as f64) - vadj.get_value()); 546 | 547 | set_source_color(cr, theme.gutter_foreground); 548 | let pango_ctx = self 549 | .da 550 | .get_pango_context() 551 | .expect("failed to get pango ctx"); 552 | let linecount_layout = 553 | self.create_layout_for_linecount(&pango_ctx, &main_state, i, padding); 554 | update_layout(cr, &linecount_layout); 555 | show_layout(cr, &linecount_layout); 556 | } 557 | } 558 | 559 | Inhibit(false) 560 | } 561 | 562 | pub fn handle_draw(&mut self, cr: &Context) -> Inhibit { 563 | // let foreground = self.main_state.borrow().theme.foreground; 564 | let theme = &self.main_state.borrow().theme; 565 | 566 | let da_width = self.da.get_allocated_width(); 567 | let da_height = self.da.get_allocated_height(); 568 | 569 | //debug!("Drawing"); 570 | // cr.select_font_face("Mono", ::cairo::enums::FontSlant::Normal, ::cairo::enums::FontWeight::Normal); 571 | // let mut font_options = cr.get_font_options(); 572 | // debug!("font options: {:?} {:?} {:?}", font_options, font_options.get_antialias(), font_options.get_hint_style()); 573 | // font_options.set_hint_style(HintStyle::Full); 574 | 575 | // let (text_width, text_height) = self.get_text_size(); 576 | let num_lines = self.line_cache.height(); 577 | 578 | let vadj = self.vadj.clone(); 579 | let hadj = self.hadj.clone(); 580 | trace!("drawing. vadj={}, {}", vadj.get_value(), vadj.get_upper()); 581 | 582 | let first_line = (vadj.get_value() / self.font_height) as u64; 583 | let last_line = ((vadj.get_value() + f64::from(da_height)) / self.font_height) as u64 + 1; 584 | let last_line = min(last_line, num_lines); 585 | 586 | // debug!("line_cache {} {} {}", self.line_cache.n_invalid_before, self.line_cache.lines.len(), self.line_cache.n_invalid_after); 587 | // let missing = self.line_cache.get_missing(first_line, last_line); 588 | 589 | // Find missing lines 590 | let mut found_missing = false; 591 | for i in first_line..last_line { 592 | if self.line_cache.get_line(i).is_none() { 593 | debug!("missing line {}", i); 594 | found_missing = true; 595 | } 596 | } 597 | 598 | // We've already missed our chance to draw these lines, but we need to request them for the 599 | // next frame. This needs to be improved to prevent flashing. 600 | if found_missing { 601 | debug!( 602 | "didn't have some lines, requesting, lines {}-{}", 603 | first_line, last_line 604 | ); 605 | self.controller.borrow().core().request_lines( 606 | &self.view_id, 607 | first_line as u64, 608 | last_line as u64, 609 | ); 610 | } 611 | 612 | let pango_ctx = self.da.get_pango_context().unwrap(); 613 | pango_ctx.set_font_description(&self.font_desc); 614 | 615 | // Draw background 616 | set_source_color(cr, theme.background); 617 | cr.rectangle(0.0, 0.0, f64::from(da_width), f64::from(da_height)); 618 | cr.fill(); 619 | 620 | set_source_color(cr, theme.foreground); 621 | 622 | // Highlight cursor lines 623 | // for i in first_line..last_line { 624 | // cr.set_source_rgba(0.8, 0.8, 0.8, 1.0); 625 | // if let Some(line) = self.line_cache.get_line(i) { 626 | 627 | // if !line.cursor().is_empty() { 628 | // cr.set_source_rgba(0.23, 0.23, 0.23, 1.0); 629 | // cr.rectangle(0f64, 630 | // font_extents.height*((i+1) as f64) - font_extents.ascent - vadj.get_value(), 631 | // da_width as f64, 632 | // font_extents.ascent + font_extents.descent); 633 | // cr.fill(); 634 | // } 635 | // } 636 | // } 637 | 638 | const CURSOR_WIDTH: f64 = 2.0; 639 | // Calculate ordinal or max line length 640 | let padding: usize = format!("{}", num_lines.saturating_sub(1)).len(); 641 | 642 | let mut max_width = 0; 643 | 644 | let main_state = self.main_state.borrow(); 645 | 646 | for i in first_line..last_line { 647 | // Keep track of the starting x position 648 | if let Some(line) = self.line_cache.get_line(i) { 649 | cr.move_to( 650 | -hadj.get_value(), 651 | self.font_height * (i as f64) - vadj.get_value(), 652 | ); 653 | 654 | let pango_ctx = self 655 | .da 656 | .get_pango_context() 657 | .expect("failed to get pango ctx"); 658 | 659 | set_source_color(cr, theme.foreground); 660 | let layout = self.create_layout_for_line(&pango_ctx, &main_state, line); 661 | max_width = max(max_width, layout.get_extents().1.width); 662 | // debug!("width={}", layout.get_extents().1.width); 663 | update_layout(cr, &layout); 664 | show_layout(cr, &layout); 665 | 666 | let layout_line = layout.get_line(0); 667 | if layout_line.is_none() { 668 | continue; 669 | } 670 | let layout_line = layout_line.unwrap(); 671 | 672 | // Draw the cursor 673 | set_source_color(cr, theme.caret); 674 | 675 | for c in line.cursor() { 676 | let x = layout_line.index_to_x(*c as i32, false) / pango::SCALE; 677 | cr.rectangle( 678 | (x as f64) - hadj.get_value(), 679 | (((self.font_height) as u64) * i) as f64 680 | - vadj.get_value(), 681 | CURSOR_WIDTH, 682 | self.font_height, 683 | ); 684 | cr.fill(); 685 | } 686 | } 687 | } 688 | 689 | // Now that we know actual length of the text, adjust the scrollbar properly. 690 | // But we need to make sure we don't make the upper value smaller than the current viewport 691 | let mut h_upper = f64::from(max_width / pango::SCALE); 692 | let cur_h_max = hadj.get_value() + hadj.get_page_size(); 693 | if cur_h_max > h_upper { 694 | h_upper = cur_h_max; 695 | } 696 | 697 | if hadj.get_upper() != h_upper { 698 | hadj.set_upper(h_upper); 699 | // If I don't signal that the value changed, sometimes the overscroll "shadow" will stick 700 | // This seems to make sure to tell the viewport that something has changed so it can 701 | // reevaluate its need for a scroll shadow. 702 | hadj.value_changed(); 703 | } 704 | 705 | Inhibit(false) 706 | } 707 | 708 | /// Creates a pango layout for a particular line number 709 | fn create_layout_for_linecount( 710 | &self, 711 | pango_ctx: &pango::Context, 712 | main_state: &MainState, 713 | n: u64, 714 | padding: usize, 715 | ) -> pango::Layout { 716 | let line_view = format!("{:>offset$} ", n, offset = padding); 717 | let layout = pango::Layout::new(pango_ctx); 718 | layout.set_font_description(Some(&self.font_desc)); 719 | layout.set_text(line_view.as_str()); 720 | layout 721 | } 722 | 723 | /// Creates a pango layout for a particular line in the linecache 724 | fn create_layout_for_line( 725 | &self, 726 | pango_ctx: &pango::Context, 727 | main_state: &MainState, 728 | line: &Line, 729 | ) -> pango::Layout { 730 | let line_view = if line.text().ends_with('\n') { 731 | &line.text()[0..line.text().len() - 1] 732 | } else { 733 | &line.text() 734 | }; 735 | 736 | // let layout = create_layout(cr).unwrap(); 737 | let layout = pango::Layout::new(pango_ctx); 738 | layout.set_font_description(Some(&self.font_desc)); 739 | layout.set_text(line_view); 740 | 741 | let mut ix = 0; 742 | let attr_list = pango::AttrList::new(); 743 | for style in &line.styles { 744 | let start_index = (ix + style.start) as u32; 745 | let end_index = (ix + style.start + style.len as i64) as u32; 746 | 747 | let foreground = main_state.styles.get(style.id).and_then(|s| s.fg_color); 748 | if let Some(foreground) = foreground { 749 | let mut attr = Attribute::new_foreground( 750 | foreground.r_u16(), 751 | foreground.g_u16(), 752 | foreground.b_u16(), 753 | ) 754 | .unwrap(); 755 | attr.set_start_index(start_index); 756 | attr.set_end_index(end_index); 757 | attr_list.insert(attr); 758 | } 759 | 760 | let background = main_state.styles.get(style.id).and_then(|s| s.bg_color); 761 | if let Some(background) = background { 762 | let mut attr = Attribute::new_background( 763 | background.r_u16(), 764 | background.g_u16(), 765 | background.b_u16(), 766 | ) 767 | .unwrap(); 768 | attr.set_start_index(start_index); 769 | attr.set_end_index(end_index); 770 | attr_list.insert(attr); 771 | } 772 | 773 | let weight = main_state.styles.get(style.id).and_then(|s| s.weight); 774 | if let Some(weight) = weight { 775 | let mut attr = 776 | Attribute::new_weight(pango::Weight::__Unknown(weight as i32)).unwrap(); 777 | attr.set_start_index(start_index); 778 | attr.set_end_index(end_index); 779 | attr_list.insert(attr); 780 | } 781 | 782 | let italic = main_state.styles.get(style.id).and_then(|s| s.italic); 783 | if let Some(italic) = italic { 784 | let mut attr = if italic { 785 | Attribute::new_style(pango::Style::Italic).unwrap() 786 | } else { 787 | Attribute::new_style(pango::Style::Normal).unwrap() 788 | }; 789 | attr.set_start_index(start_index); 790 | attr.set_end_index(end_index); 791 | attr_list.insert(attr); 792 | } 793 | 794 | let underline = main_state.styles.get(style.id).and_then(|s| s.underline); 795 | if let Some(underline) = underline { 796 | let mut attr = if underline { 797 | Attribute::new_underline(pango::Underline::Single).unwrap() 798 | } else { 799 | Attribute::new_underline(pango::Underline::None).unwrap() 800 | }; 801 | attr.set_start_index(start_index); 802 | attr.set_end_index(end_index); 803 | attr_list.insert(attr); 804 | } 805 | 806 | ix += style.start + style.len as i64; 807 | } 808 | 809 | layout.set_attributes(Some(&attr_list)); 810 | layout 811 | } 812 | 813 | pub fn scroll_to(edit_view: &Rc>, line: u64, col: u64) { 814 | // We can't have edit_view borrowed when we call set_value on adjustments 815 | // because set_value can call the value_changed handlers. So first, we 816 | // need to extract the information we're going to need. 817 | let (cur_top, cur_bottom, vadj, cur_left, cur_right, hadj) = { 818 | let ev = edit_view.borrow(); 819 | let cur_top = ev.font_height * ((line + 1) as f64) - ev.font_ascent; 820 | let cur_bottom = cur_top + ev.font_ascent + ev.font_descent; 821 | let vadj = ev.vadj.clone(); 822 | 823 | let cur_left = ev.font_width * (col as f64) - ev.font_ascent; 824 | let cur_right = cur_left + ev.font_width * 2.0; 825 | let hadj = ev.hadj.clone(); 826 | 827 | (cur_top, cur_bottom, vadj, cur_left, cur_right, hadj) 828 | }; 829 | 830 | if cur_top < vadj.get_value() { 831 | vadj.set_value(cur_top); 832 | } else if cur_bottom > vadj.get_value() + vadj.get_page_size() 833 | && vadj.get_page_size() != 0.0 834 | { 835 | vadj.set_value(cur_bottom - vadj.get_page_size()); 836 | } 837 | 838 | if cur_left < hadj.get_value() { 839 | hadj.set_value(cur_left); 840 | } else if cur_right > hadj.get_value() + hadj.get_page_size() && hadj.get_page_size() != 0.0 841 | { 842 | let new_value = cur_right - hadj.get_page_size(); 843 | if new_value + hadj.get_page_size() > hadj.get_upper() { 844 | hadj.set_upper(new_value + hadj.get_page_size()); 845 | } 846 | hadj.set_value(new_value); 847 | } 848 | } 849 | 850 | pub fn handle_button_press(&self, eb: &EventButton) -> Inhibit { 851 | self.da.grab_focus(); 852 | 853 | let (x, y) = eb.get_position(); 854 | let (col, line) = { 855 | let main_state = self.main_state.borrow(); 856 | self.da_px_to_cell(&main_state, x, y) 857 | }; 858 | 859 | match eb.get_button() { 860 | 1 => { 861 | if eb.get_state().contains(ModifierType::SHIFT_MASK) { 862 | self.controller 863 | .borrow() 864 | .core() 865 | .gesture_range_select(&self.view_id, line, col); 866 | } else if eb.get_state().contains(ModifierType::CONTROL_MASK) { 867 | self.controller 868 | .borrow() 869 | .core() 870 | .gesture_toggle_sel(&self.view_id, line, col); 871 | } else if eb.get_event_type() == EventType::DoubleButtonPress { 872 | self.controller 873 | .borrow() 874 | .core() 875 | .gesture_word_select(&self.view_id, line, col); 876 | } else if eb.get_event_type() == EventType::TripleButtonPress { 877 | self.controller 878 | .borrow() 879 | .core() 880 | .gesture_line_select(&self.view_id, line, col); 881 | } else { 882 | self.controller 883 | .borrow() 884 | .core() 885 | .gesture_point_select(&self.view_id, line, col); 886 | } 887 | } 888 | 2 => { 889 | self.do_paste_primary(&self.view_id, line, col); 890 | } 891 | _ => {} 892 | } 893 | Inhibit(false) 894 | } 895 | 896 | pub fn handle_drag(&mut self, em: &EventMotion) -> Inhibit { 897 | let (x, y) = em.get_position(); 898 | let (col, line) = { 899 | let main_state = self.main_state.borrow(); 900 | self.da_px_to_cell(&main_state, x, y) 901 | }; 902 | self.controller.borrow().core().drag( 903 | &self.view_id, 904 | line, 905 | col, 906 | convert_gtk_modifier(em.get_state()), 907 | ); 908 | Inhibit(false) 909 | } 910 | 911 | pub fn handle_scroll(&mut self, es: &EventScroll) -> Inhibit { 912 | // self.da.grab_focus(); 913 | // // let amt = self.font_height * 3.0; 914 | 915 | // if let ScrollDirection::Smooth = es.get_direction() { 916 | // error!("Smooth scroll!"); 917 | // } 918 | 919 | // debug!("handle scroll {:?}", es); 920 | // let vadj = self.vadj.clone(); 921 | // let hadj = self.hadj.clone(); 922 | // match es.get_direction() { 923 | // ScrollDirection::Up => vadj.set_value(vadj.get_value() - amt), 924 | // ScrollDirection::Down => vadj.set_value(vadj.get_value() + amt), 925 | // ScrollDirection::Left => hadj.set_value(hadj.get_value() - amt), 926 | // ScrollDirection::Right => hadj.set_value(hadj.get_value() + amt), 927 | // ScrollDirection::Smooth => debug!("scroll Smooth"), 928 | // _ => {}, 929 | // } 930 | 931 | self.update_visible_scroll_region(); 932 | 933 | Inhibit(false) 934 | } 935 | 936 | fn handle_key_press_event(&mut self, ek: &EventKey) -> Inhibit { 937 | debug!( 938 | "key press keyval={:?}, state={:?}, length={:?} group={:?} uc={:?}", 939 | ek.get_keyval(), 940 | ek.get_state(), 941 | ek.get_length(), 942 | ek.get_group(), 943 | ::gdk::keyval_to_unicode(ek.get_keyval()) 944 | ); 945 | let view_id = &self.view_id; 946 | let ch = ::gdk::keyval_to_unicode(ek.get_keyval()); 947 | 948 | let alt = ek.get_state().contains(ModifierType::MOD1_MASK); 949 | let ctrl = ek.get_state().contains(ModifierType::CONTROL_MASK); 950 | let meta = ek.get_state().contains(ModifierType::META_MASK); 951 | let shift = ek.get_state().contains(ModifierType::SHIFT_MASK); 952 | let norm = !alt && !ctrl && !meta; 953 | 954 | match ek.get_keyval() { 955 | key::Delete if norm => self.controller.borrow().core().delete_forward(view_id), 956 | key::BackSpace if norm => self.controller.borrow().core().delete_backward(view_id), 957 | key::Return | key::KP_Enter => { 958 | self.controller.borrow().core().insert_newline(&view_id); 959 | } 960 | key::Tab if norm && !shift => self.controller.borrow().core().insert_tab(view_id), 961 | key::Up if norm && !shift => self.controller.borrow().core().move_up(view_id), 962 | key::Down if norm && !shift => self.controller.borrow().core().move_down(view_id), 963 | key::Left if norm && !shift => self.controller.borrow().core().move_left(view_id), 964 | key::Right if norm && !shift => self.controller.borrow().core().move_right(view_id), 965 | key::Up if norm && shift => { 966 | self.controller 967 | .borrow() 968 | .core() 969 | .move_up_and_modify_selection(view_id); 970 | } 971 | key::Down if norm && shift => { 972 | self.controller 973 | .borrow() 974 | .core() 975 | .move_down_and_modify_selection(view_id); 976 | } 977 | key::Left if norm && shift => { 978 | self.controller 979 | .borrow() 980 | .core() 981 | .move_left_and_modify_selection(view_id); 982 | } 983 | key::Right if norm && shift => { 984 | self.controller 985 | .borrow() 986 | .core() 987 | .move_right_and_modify_selection(view_id); 988 | } 989 | key::Left if ctrl && !shift => { 990 | self.controller.borrow().core().move_word_left(view_id); 991 | } 992 | key::Right if ctrl && !shift => { 993 | self.controller.borrow().core().move_word_right(view_id); 994 | } 995 | key::Left if ctrl && shift => { 996 | self.controller 997 | .borrow() 998 | .core() 999 | .move_word_left_and_modify_selection(view_id); 1000 | } 1001 | key::Right if ctrl && shift => { 1002 | self.controller 1003 | .borrow() 1004 | .core() 1005 | .move_word_right_and_modify_selection(view_id); 1006 | } 1007 | key::Home if norm && !shift => { 1008 | self.controller 1009 | .borrow() 1010 | .core() 1011 | .move_to_left_end_of_line(view_id); 1012 | } 1013 | key::End if norm && !shift => { 1014 | self.controller 1015 | .borrow() 1016 | .core() 1017 | .move_to_right_end_of_line(view_id); 1018 | } 1019 | key::Home if norm && shift => { 1020 | self.controller 1021 | .borrow() 1022 | .core() 1023 | .move_to_left_end_of_line_and_modify_selection(view_id); 1024 | } 1025 | key::End if norm && shift => { 1026 | self.controller 1027 | .borrow() 1028 | .core() 1029 | .move_to_right_end_of_line_and_modify_selection(view_id); 1030 | } 1031 | key::Home if ctrl && !shift => { 1032 | self.controller 1033 | .borrow() 1034 | .core() 1035 | .move_to_beginning_of_document(view_id); 1036 | } 1037 | key::End if ctrl && !shift => { 1038 | self.controller 1039 | .borrow() 1040 | .core() 1041 | .move_to_end_of_document(view_id); 1042 | } 1043 | key::Home if ctrl && shift => { 1044 | self.controller 1045 | .borrow() 1046 | .core() 1047 | .move_to_beginning_of_document_and_modify_selection(view_id); 1048 | } 1049 | key::End if ctrl && shift => { 1050 | self.controller 1051 | .borrow() 1052 | .core() 1053 | .move_to_end_of_document_and_modify_selection(view_id); 1054 | } 1055 | key::Page_Up if norm && !shift => { 1056 | self.controller.borrow().core().page_up(view_id); 1057 | } 1058 | key::Page_Down if norm && !shift => { 1059 | self.controller.borrow().core().page_down(view_id); 1060 | } 1061 | key::Page_Up if norm && shift => { 1062 | self.controller 1063 | .borrow() 1064 | .core() 1065 | .page_up_and_modify_selection(view_id); 1066 | } 1067 | key::Page_Down if norm && shift => { 1068 | self.controller 1069 | .borrow() 1070 | .core() 1071 | .page_down_and_modify_selection(view_id); 1072 | } 1073 | _ => { 1074 | if let Some(ch) = ch { 1075 | match ch { 1076 | 'a' if ctrl => { 1077 | self.controller.borrow().core().select_all(view_id); 1078 | } 1079 | 'c' if ctrl => { 1080 | self.do_copy(view_id); 1081 | } 1082 | 'f' if ctrl => { 1083 | self.start_search(); 1084 | } 1085 | 'v' if ctrl => { 1086 | self.do_paste(view_id); 1087 | } 1088 | 't' if ctrl => { 1089 | // TODO new tab 1090 | } 1091 | 'x' if ctrl => { 1092 | self.do_cut(view_id); 1093 | } 1094 | 'z' if ctrl => { 1095 | self.controller.borrow().core().undo(view_id); 1096 | } 1097 | 'Z' if ctrl && shift => { 1098 | self.controller.borrow().core().redo(view_id); 1099 | } 1100 | c if (norm) && c >= '\u{0020}' => { 1101 | debug!("inserting key"); 1102 | self.controller 1103 | .borrow() 1104 | .core() 1105 | .insert(view_id, &c.to_string()); 1106 | } 1107 | _ => { 1108 | debug!("unhandled key: {:?}", ch); 1109 | } 1110 | } 1111 | } 1112 | } 1113 | }; 1114 | Inhibit(true) 1115 | } 1116 | 1117 | fn do_cut(&self, view_id: &str) { 1118 | if let Some(text) = self.controller.borrow().core().cut(view_id) { 1119 | Clipboard::get(&SELECTION_CLIPBOARD).set_text(&text); 1120 | } 1121 | } 1122 | 1123 | fn do_copy(&self, view_id: &str) { 1124 | if let Some(text) = self.controller.borrow().core().copy(view_id) { 1125 | Clipboard::get(&SELECTION_CLIPBOARD).set_text(&text); 1126 | } 1127 | } 1128 | 1129 | fn do_paste(&self, view_id: &str) { 1130 | let view_id2 = view_id.to_string().clone(); 1131 | let core = self.controller.borrow().core().clone(); 1132 | Clipboard::get(&SELECTION_CLIPBOARD).request_text(move |_, text| { 1133 | if let Some(text) = text { 1134 | core.paste(&view_id2, &text); 1135 | } 1136 | }); 1137 | } 1138 | 1139 | fn do_paste_primary(&self, view_id: &str, line: u64, col: u64) { 1140 | let view_id2 = view_id.to_string().clone(); 1141 | let core = self.controller.borrow().core().clone(); 1142 | Clipboard::get(&SELECTION_PRIMARY).request_text(move |_, text| { 1143 | if let Some(text) = text { 1144 | core.gesture_point_select(&view_id2, line, col); 1145 | core.insert(&view_id2, text); 1146 | } 1147 | }); 1148 | } 1149 | 1150 | pub fn start_search(&self) { 1151 | self.search_bar.set_search_mode(true); 1152 | self.replace_expander.set_expanded(false); 1153 | self.replace_revealer.set_reveal_child(false); 1154 | self.search_entry.grab_focus(); 1155 | let needle = self 1156 | .search_entry 1157 | .get_text() 1158 | .map(|gs| gs.as_str().to_owned()) 1159 | .unwrap_or_default(); 1160 | self.controller 1161 | .borrow() 1162 | .core() 1163 | .find(&self.view_id, needle, false, Some(false)); 1164 | } 1165 | 1166 | pub fn stop_search(&self) { 1167 | self.search_bar.set_search_mode(false); 1168 | self.da.grab_focus(); 1169 | } 1170 | 1171 | pub fn find_status(&self, queries: &Value) { 1172 | if let Some(queries) = queries.as_array() { 1173 | for query in queries { 1174 | if let Some(query_obj) = query.as_object() { 1175 | if let Some(matches) = query_obj["matches"].as_u64() { 1176 | self.find_status_label 1177 | .set_text(&format!("{} Results", matches)); 1178 | } 1179 | } 1180 | debug!("query {}", query); 1181 | } 1182 | } 1183 | } 1184 | 1185 | pub fn find_next(&self) { 1186 | self.controller 1187 | .borrow() 1188 | .core() 1189 | .find_next(&self.view_id, Some(true), Some(true)); 1190 | } 1191 | 1192 | pub fn find_prev(&self) { 1193 | self.controller 1194 | .borrow() 1195 | .core() 1196 | .find_previous(&self.view_id, Some(true)); 1197 | } 1198 | 1199 | pub fn search_changed(&self, s: Option) { 1200 | let needle = s.unwrap_or_default(); 1201 | self.controller 1202 | .borrow() 1203 | .core() 1204 | .find(&self.view_id, needle, false, Some(false)); 1205 | } 1206 | 1207 | pub fn replace(&self) { 1208 | let replace_chars = self 1209 | .replace_entry 1210 | .get_text() 1211 | .map(|gs| gs.as_str().to_owned()) 1212 | .unwrap_or_default(); 1213 | self.controller 1214 | .borrow() 1215 | .core() 1216 | .replace(&self.view_id, &replace_chars, false); 1217 | self.controller.borrow().core().replace_next(&self.view_id); 1218 | } 1219 | 1220 | pub fn replace_all(&self) { 1221 | let replace_chars = self 1222 | .replace_entry 1223 | .get_text() 1224 | .map(|gs| gs.as_str().to_owned()) 1225 | .unwrap_or_default(); 1226 | self.controller 1227 | .borrow() 1228 | .core() 1229 | .replace(&self.view_id, &replace_chars, false); 1230 | self.controller.borrow().core().replace_all(&self.view_id); 1231 | } 1232 | } 1233 | -------------------------------------------------------------------------------- /src/linecache.rs: -------------------------------------------------------------------------------- 1 | use log::*; 2 | use serde_json::Value; 3 | use std::cmp::min; 4 | use std::collections::HashMap; 5 | 6 | #[derive(Copy, Clone, Debug)] 7 | pub struct StyleSpan { 8 | pub start: i64, 9 | pub len: usize, 10 | pub id: usize, 11 | } 12 | 13 | #[derive(Clone, Debug)] 14 | pub struct Line { 15 | text: String, 16 | cursor: Vec, 17 | pub styles: Vec, 18 | } 19 | 20 | impl Line { 21 | pub fn from_json(v: &Value) -> Line { 22 | let text = v["text"].as_str().unwrap().to_owned(); 23 | let mut cursor = Vec::new(); 24 | if let Some(arr) = v["cursor"].as_array() { 25 | for c in arr { 26 | cursor.push(c.as_u64().unwrap() as usize); 27 | } 28 | } 29 | let mut styles = Vec::new(); 30 | if let Some(arr) = v["styles"].as_array() { 31 | // Convert style triples into a `Vec` of `StyleSpan`s 32 | let mut i = 0; 33 | let mut style_span = StyleSpan { 34 | start: 0, 35 | len: 0, 36 | id: 0, 37 | }; 38 | for c in arr { 39 | if i == 3 { 40 | i = 0; 41 | styles.push(style_span); 42 | } 43 | match i { 44 | 0 => style_span.start = c.as_i64().unwrap() as i64, 45 | 1 => style_span.len = c.as_u64().unwrap() as usize, 46 | 2 => style_span.id = c.as_u64().unwrap() as usize, 47 | _ => unreachable!(), 48 | }; 49 | i += 1; 50 | } 51 | if i == 3 { 52 | styles.push(style_span); 53 | } 54 | } 55 | Line { 56 | text, 57 | cursor, 58 | styles, 59 | } 60 | } 61 | 62 | pub fn text(&self) -> &str { 63 | &self.text 64 | } 65 | 66 | pub fn cursor(&self) -> &[usize] { 67 | &self.cursor 68 | } 69 | } 70 | 71 | #[derive(Debug)] 72 | pub struct LineCache { 73 | map: HashMap, 74 | pub n_invalid_before: u64, 75 | pub lines: Vec>, 76 | pub n_invalid_after: u64, 77 | } 78 | 79 | impl LineCache { 80 | pub fn new() -> LineCache { 81 | LineCache { 82 | map: HashMap::new(), 83 | n_invalid_before: 0, 84 | lines: Vec::new(), 85 | n_invalid_after: 0, 86 | } 87 | } 88 | pub fn height(&self) -> u64 { 89 | self.n_invalid_before + self.lines.len() as u64 + self.n_invalid_after 90 | } 91 | pub fn width(&self) -> usize { 92 | self.lines 93 | .iter() 94 | .map(|l| match *l { 95 | None => 0, 96 | Some(ref l) => l.text.len(), 97 | }) 98 | .max() 99 | .unwrap_or(0) 100 | } 101 | pub fn get_line(&self, n: u64) -> Option<&Line> { 102 | if n < self.n_invalid_before || n > self.n_invalid_before + self.lines.len() as u64 { 103 | return None; 104 | } 105 | let ix = (n - self.n_invalid_before) as usize; 106 | if let Some(&Some(ref line)) = self.lines.get(ix) { 107 | Some(line) 108 | } else { 109 | None 110 | } 111 | } 112 | pub fn get_missing(&self, first: u64, last: u64) -> Vec<(u64, u64)> { 113 | let mut ret = Vec::new(); 114 | let last = min(last, self.height()); 115 | assert!(first < last); 116 | 117 | let mut run = None; 118 | for ix in first..last { 119 | if ix < self.n_invalid_before 120 | || ix >= self.n_invalid_before + self.lines.len() as u64 121 | || self.lines[(ix - self.n_invalid_before) as usize].is_none() 122 | { 123 | match run { 124 | None => { 125 | run = Some((ix, ix + 1)); 126 | } 127 | Some((f, l)) if l == ix => { 128 | run = Some((f, ix + 1)); 129 | } 130 | Some((f, l)) => { 131 | ret.push((f, l)); 132 | run = Some((ix, ix + 1)); 133 | } 134 | } 135 | } 136 | } 137 | if let Some((f, l)) = run { 138 | ret.push((f, l)); 139 | } 140 | ret 141 | } 142 | pub fn apply_update(&mut self, update: &Value) { 143 | let mut new_invalid_before = 0; 144 | let mut new_lines: Vec> = Vec::new(); 145 | let mut new_invalid_after = 0; 146 | 147 | let mut old_ix = 0u64; 148 | 149 | for op in update["ops"].as_array().unwrap() { 150 | let op_type = &op["op"]; 151 | //debug!("lc before {}-- {} {:?} {}", op_type, new_invalid_before, new_lines, new_invalid_after); 152 | let n = op["n"].as_u64().unwrap(); 153 | match op_type.as_str().unwrap() { 154 | "invalidate" => { 155 | trace!("invalidate n={}", n); 156 | if new_lines.is_empty() { 157 | new_invalid_before += n; 158 | } else { 159 | new_invalid_after += n; 160 | } 161 | } 162 | "ins" => { 163 | trace!("ins n={}", n); 164 | for _ in 0..new_invalid_after { 165 | new_lines.push(None); 166 | } 167 | new_invalid_after = 0; 168 | //let json_lines = op.lines.unwrap_or_else(Vec::new); 169 | // for json_line in op.lines.iter().flat_map(|l| l.iter()) { 170 | for line in op["lines"].as_array().unwrap() { 171 | let line = Line::from_json(line); 172 | new_lines.push(Some(Line { 173 | cursor: line.cursor.clone(), 174 | text: line.text.clone(), 175 | styles: line.styles.clone(), 176 | })); 177 | } 178 | } 179 | "copy" => { 180 | trace!("copy n={}", n); 181 | 182 | for _ in 0..new_invalid_after { 183 | new_lines.push(None); 184 | } 185 | new_invalid_after = 0; 186 | 187 | let mut n_remaining = n; 188 | if old_ix < self.n_invalid_before { 189 | let n_invalid = min(n, self.n_invalid_before - old_ix); 190 | if new_lines.is_empty() { 191 | new_invalid_before += n_invalid; 192 | } else { 193 | new_invalid_after += n_invalid; 194 | } 195 | old_ix += n_invalid; 196 | n_remaining -= n_invalid; 197 | } 198 | if n_remaining > 0 && old_ix < self.n_invalid_before + self.lines.len() as u64 { 199 | let n_copy = min( 200 | n_remaining, 201 | self.n_invalid_before + self.lines.len() as u64 - old_ix, 202 | ); 203 | let start_ix = old_ix - self.n_invalid_before; 204 | 205 | for i in start_ix as usize..(start_ix + n_copy) as usize { 206 | if self.lines[i].is_none() { 207 | error!( 208 | "line {}+{}={}, a copy source is none", 209 | self.n_invalid_before, 210 | i, 211 | self.n_invalid_before + i as u64 212 | ); 213 | } 214 | } 215 | new_lines.extend_from_slice( 216 | &self.lines[start_ix as usize..(start_ix + n_copy) as usize], 217 | ); 218 | 219 | old_ix += n_copy; 220 | n_remaining -= n_copy; 221 | } 222 | if new_lines.is_empty() { 223 | new_invalid_before += n_remaining; 224 | } else { 225 | new_invalid_after += n_remaining; 226 | } 227 | old_ix += n_remaining; 228 | } 229 | "skip" => { 230 | trace!("skip n={}", n); 231 | old_ix += n; 232 | } 233 | _ => {} 234 | } 235 | } 236 | self.n_invalid_before = new_invalid_before; 237 | self.lines = new_lines; 238 | self.n_invalid_after = new_invalid_after; 239 | //debug!("lc after update {:?}", self); 240 | } 241 | } 242 | 243 | #[cfg(test)] 244 | mod test { 245 | use super::*; 246 | use serde_json::json; 247 | 248 | #[test] 249 | fn test() { 250 | let mut linecache = LineCache::new(); 251 | linecache.apply_update(&json!({ 252 | "ops": [ 253 | {"op":"invalidate", "n": 20}, 254 | {"op":"ins", "n": 30, "lines": [ 255 | {"text": "20\n"}, 256 | {"text": "21\n"}, 257 | ]}, 258 | {"op":"invalidate", "n": 10}, 259 | {"op":"ins", "n": 30, "lines": [ 260 | {"text": "32\n"}, 261 | {"text": "33\n"}, 262 | ]}, 263 | {"op":"invalidate", "n": 10}, 264 | {"op":"ins", "n": 30, "lines": [ 265 | {"text": "44\n"}, 266 | {"text": "45\n"}, 267 | {"text": "46\n"}, 268 | {"text": "47\n"}, 269 | ]}, 270 | {"op":"ins", "n": 30, "lines": [ 271 | {"text": "48\n"}, 272 | {"text": "49\n"}, 273 | {"text": "50\n"}, 274 | {"text": "51\n"}, 275 | {"text": "52\n"}, 276 | ]}, 277 | {"op":"invalidate", "n": 10}, 278 | ] 279 | })); 280 | 281 | linecache.apply_update(&json!({ 282 | "ops": [ 283 | {"n":10,"op":"invalidate"}, 284 | {"n":10,"op":"invalidate"}, 285 | {"n":20,"op":"skip"}, 286 | {"n":2,"op":"copy"}, 287 | {"n":10,"op":"invalidate"}, 288 | {"n":10,"op":"skip"}, 289 | {"n":3,"op":"copy"}, 290 | {"n":10,"op":"invalidate"}, 291 | {"n":10,"op":"skip"}, 292 | {"n":4,"op":"copy"}, 293 | {"n":5,"op":"copy"}, 294 | {"n":2,"op":"ins", 295 | "lines":[ 296 | {"styles":[0,70,2],"text":"53\n"}, 297 | {"styles":[0,72,2],"text":"54\n"}, 298 | ]}, 299 | {"n":8,"op":"invalidate"}, 300 | ] 301 | })); 302 | 303 | assert_eq!(linecache.n_invalid_before, 20); 304 | assert_eq!(linecache.n_invalid_after, 8); 305 | assert_eq!(linecache.get_line(20).unwrap().text(), "20\n"); 306 | assert_eq!(linecache.get_line(21).unwrap().text(), "21\n"); 307 | assert!(linecache.get_line(22).is_none()); 308 | assert_eq!(linecache.get_line(32).unwrap().text(), "32\n"); 309 | assert_eq!(linecache.get_line(52).unwrap().text(), "52\n"); 310 | assert!(linecache.get_line(53).is_none()); 311 | 312 | println!("LINE CACHE: {:?}", linecache); 313 | } 314 | } 315 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![recursion_limit = "128"] 2 | 3 | mod channel; 4 | mod clipboard; 5 | mod controller; 6 | mod edit_view; 7 | mod linecache; 8 | mod main_win; 9 | mod prefs_win; 10 | mod proto; 11 | mod rpc; 12 | mod scrollable_drawing_area; 13 | mod theme; 14 | mod xi_thread; 15 | 16 | use crate::channel::Sender; 17 | use crate::controller::{Controller, CoreMsg}; 18 | use clap::{Arg, SubCommand}; 19 | use gio::prelude::*; 20 | use gio::{ApplicationExt, ApplicationFlags, FileExt}; 21 | use glib::clone; 22 | use gtk::{Application}; 23 | use log::*; 24 | use main_win::MainWin; 25 | use rpc::{Core, Handler}; 26 | use serde_json::Value; 27 | use std::any::Any; 28 | use std::cell::RefCell; 29 | use std::env::args; 30 | use dirs_next::home_dir; 31 | 32 | // pub struct SharedQueue { 33 | // queue: VecDeque, 34 | // } 35 | 36 | // impl SharedQueue { 37 | // pub fn add_core_msg(&mut self, msg: CoreMsg) { 38 | // if self.queue.is_empty() { 39 | // self.pipe_writer 40 | // .write_all(&[0u8]) 41 | // .expect("failed to write to signalling pipe"); 42 | // } 43 | // trace!("pushing to queue"); 44 | // self.queue.push_back(msg); 45 | // } 46 | // } 47 | 48 | trait IdleCallback: Send { 49 | fn call(self: Box, a: &Any); 50 | } 51 | 52 | impl IdleCallback for F { 53 | fn call(self: Box, a: &Any) { 54 | (*self)(a) 55 | } 56 | } 57 | 58 | // struct QueueSource { 59 | // win: Rc>, 60 | // sender: Sender, 61 | // } 62 | 63 | // impl SourceFuncs for QueueSource { 64 | // fn check(&self) -> bool { 65 | // false 66 | // } 67 | 68 | // fn prepare(&self) -> (bool, Option) { 69 | // (false, None) 70 | // } 71 | 72 | // fn dispatch(&self) -> bool { 73 | // trace!("dispatch"); 74 | // let mut shared_queue = self.queue.lock().unwrap(); 75 | // while let Some(msg) = shared_queue.queue.pop_front() { 76 | // trace!("found a msg"); 77 | // MainWin::handle_msg(self.win.clone(), msg); 78 | // } 79 | // let mut buf = [0u8; 64]; 80 | // shared_queue 81 | // .pipe_reader 82 | // .try_read(&mut buf) 83 | // .expect("failed to read signalling pipe"); 84 | // true 85 | // } 86 | // } 87 | 88 | #[derive(Clone)] 89 | struct MyHandler { 90 | sender: Sender, 91 | } 92 | 93 | impl MyHandler { 94 | fn new(sender: Sender) -> MyHandler { 95 | MyHandler { sender } 96 | } 97 | } 98 | 99 | impl Handler for MyHandler { 100 | fn notification(&self, method: &str, params: &Value) { 101 | debug!( 102 | "CORE --> {{\"method\": \"{}\", \"params\":{}}}", 103 | method, params 104 | ); 105 | let method2 = method.to_string(); 106 | let params2 = params.clone(); 107 | self.sender.send(CoreMsg::Notification { 108 | method: method2, 109 | params: params2, 110 | }); 111 | } 112 | } 113 | 114 | fn main() { 115 | env_logger::init(); 116 | // let matches = App::new("gxi") 117 | // .version("0.2.0") 118 | // .author("brainn ") 119 | // .about("Xi frontend") 120 | // .arg(Arg::with_name("FILE") 121 | // .multiple(true) 122 | // .help("file to open") 123 | // ) 124 | // .get_matches(); 125 | 126 | // let mut files = vec![]; 127 | // if matches.is_present("FILE") { 128 | // files = matches.values_of("FILE").unwrap().collect::>(); 129 | // } 130 | // debug!("files {:?}", files); 131 | 132 | let controller = Controller::new(); 133 | let controller2 = controller.clone(); 134 | let (chan, sender) = channel::Channel::new(move |msg| { 135 | controller2.borrow().handle_msg(msg); 136 | }); 137 | controller.borrow_mut().set_sender(sender.clone()); 138 | controller.borrow_mut().set_channel(chan); 139 | 140 | // let queue: VecDeque = Default::default(); 141 | // let (reader, writer) = pipe().unwrap(); 142 | // let reader_raw_fd = reader.as_raw_fd(); 143 | 144 | // let shared_queue = Arc::new(Mutex::new(SharedQueue { 145 | // queue: queue.clone(), 146 | // pipe_writer: writer, 147 | // pipe_reader: reader, 148 | // })); 149 | 150 | let (xi_peer, rx) = xi_thread::start_xi_thread(); 151 | let handler = MyHandler::new(sender.clone()); 152 | let core = Core::new(xi_peer, rx, handler.clone()); 153 | controller.borrow_mut().set_core(core); 154 | 155 | let application = 156 | Application::new(Some("com.github.bvinc.gxi"), ApplicationFlags::HANDLES_OPEN) 157 | .expect("failed to create gtk application"); 158 | 159 | let mut config_dir = None; 160 | let mut plugin_dir = None; 161 | if let Some(home_dir) = home_dir() { 162 | let xi_config = home_dir.join(".config").join("xi"); 163 | let xi_plugin = xi_config.join("plugins"); 164 | config_dir = xi_config.to_str().map(|s| s.to_string()); 165 | plugin_dir = xi_plugin.to_str().map(|s| s.to_string()); 166 | } 167 | 168 | application.connect_startup(clone!(@strong controller => move |application| { 169 | debug!("startup"); 170 | controller.borrow().core().client_started(config_dir.clone(), plugin_dir.clone()); 171 | 172 | let main_win = MainWin::new(application, controller.clone()); 173 | controller.borrow_mut().set_main_win(main_win); 174 | 175 | // let source = new_source(QueueSource { 176 | // win: main_win.clone(), 177 | // sender: sender.clone(), 178 | // }); 179 | // unsafe { 180 | // use glib::translate::ToGlibPtr; 181 | // ::glib_sys::g_source_add_unix_fd(source.to_glib_none().0, reader_raw_fd, ::glib_sys::G_IO_IN); 182 | // } 183 | // let main_context = MainContext::default(); 184 | // source.attach(Some(&main_context)); 185 | })); 186 | 187 | application.connect_activate(clone!(@strong controller => move |application| { 188 | debug!("activate"); 189 | 190 | controller.borrow().req_new_view(None); 191 | })); 192 | 193 | application.connect_open(clone!(@strong controller => move |_,files,s| { 194 | debug!("open"); 195 | 196 | for file in files { 197 | let path = file.get_path(); 198 | if path.is_none() { continue; } 199 | let path = path.unwrap(); 200 | let path = path.to_string_lossy().into_owned(); 201 | 202 | controller.borrow().req_new_view(Some(&path)); 203 | } 204 | })); 205 | application.connect_shutdown(move |_| { 206 | debug!("shutdown"); 207 | }); 208 | 209 | application.run(&args().collect::>()); 210 | } 211 | -------------------------------------------------------------------------------- /src/main_win.rs: -------------------------------------------------------------------------------- 1 | use crate::controller::ControllerRef; 2 | use crate::edit_view::EditView; 3 | use crate::prefs_win::PrefsWin; 4 | use crate::proto::{self, ThemeSettings}; 5 | use crate::theme::{Color, Style, Theme}; 6 | use gio::{ActionMapExt, SimpleAction}; 7 | use glib::clone; 8 | use gtk::prelude::*; 9 | use gtk::*; 10 | use log::*; 11 | use serde_json::{self, Value}; 12 | use std::cell::RefCell; 13 | use std::collections::{BTreeMap, HashMap}; 14 | use std::rc::Rc; 15 | 16 | pub struct MainState { 17 | pub themes: Vec, 18 | pub theme_name: String, 19 | pub theme: Theme, 20 | pub styles: Vec 110 | 111 | 112 | False 113 | True 114 | 0 115 | 116 | 117 | 118 | 119 | True 120 | False 121 | No Results 122 | 16 123 | 0 124 | 125 | 126 | False 127 | True 128 | 1 129 | 130 | 131 | 132 | 133 | False 134 | True 135 | 0 136 | 137 | 138 | 139 | 140 | True 141 | False 142 | 143 | 144 | True 145 | False 146 | 8 147 | 148 | 149 | 320 150 | True 151 | True 152 | edit-find-replace-symbolic 153 | Replace 154 | 155 | 156 | False 157 | True 158 | 0 159 | 160 | 161 | 162 | 163 | Replace 164 | True 165 | True 166 | True 167 | 168 | 169 | False 170 | True 171 | 1 172 | 173 | 174 | 175 | 176 | Replace All 177 | True 178 | True 179 | True 180 | 181 | 182 | False 183 | True 184 | 2 185 | 186 | 187 | 188 | 189 | 190 | 191 | False 192 | True 193 | 2 194 | 195 | 196 | 197 | 198 | False 199 | False 200 | 1 201 | 202 | 203 | 204 | 205 | 206 | 207 | -------------------------------------------------------------------------------- /src/ui/gxi.glade: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | False 7 | 8 | 9 | True 10 | False 11 | 8 12 | 8 13 | 8 14 | 8 15 | vertical 16 | 17 | 18 | True 19 | True 20 | True 21 | app.save_as 22 | Save As... 23 | 24 | 25 | False 26 | True 27 | 0 28 | 29 | 30 | 31 | 32 | True 33 | True 34 | True 35 | app.save_all 36 | Save All 37 | 38 | 39 | False 40 | True 41 | 1 42 | 43 | 44 | 45 | 46 | True 47 | False 48 | 49 | 50 | False 51 | True 52 | 2 53 | 54 | 55 | 56 | 57 | True 58 | True 59 | True 60 | app.find 61 | Find... 62 | 63 | 64 | False 65 | True 66 | 3 67 | 68 | 69 | 70 | 71 | True 72 | False 73 | 74 | 75 | False 76 | True 77 | 4 78 | 79 | 80 | 81 | 82 | True 83 | True 84 | True 85 | app.auto_indent 86 | Auto Indent 87 | 88 | 89 | False 90 | True 91 | 5 92 | 93 | 94 | 95 | 96 | True 97 | False 98 | 99 | 100 | False 101 | True 102 | 6 103 | 104 | 105 | 106 | 107 | True 108 | True 109 | True 110 | app.prefs 111 | Preferences 112 | 113 | 114 | False 115 | True 116 | 7 117 | 118 | 119 | 120 | 121 | True 122 | False 123 | 124 | 125 | False 126 | True 127 | 8 128 | 129 | 130 | 131 | 132 | True 133 | True 134 | True 135 | app.help 136 | Help 137 | 138 | 139 | False 140 | True 141 | 9 142 | 143 | 144 | 145 | 146 | True 147 | True 148 | True 149 | app.about 150 | About 151 | 152 | 153 | False 154 | True 155 | 10 156 | 157 | 158 | 159 | 160 | True 161 | False 162 | 163 | 164 | False 165 | True 166 | 11 167 | 168 | 169 | 170 | 171 | True 172 | True 173 | True 174 | app.close_all 175 | Close All 176 | 177 | 178 | False 179 | True 180 | 12 181 | 182 | 183 | 184 | 185 | True 186 | True 187 | True 188 | app.close 189 | Close 190 | 191 | 192 | False 193 | True 194 | 13 195 | 196 | 197 | 198 | 199 | True 200 | True 201 | True 202 | app.quit 203 | Quit 204 | 205 | 206 | False 207 | True 208 | 14 209 | 210 | 211 | 212 | 213 | main 214 | 1 215 | 216 | 217 | 218 | 219 | True 220 | False 221 | tab-new-symbolic 222 | 223 | 224 | False 225 | com.github.bvinc.gxi 226 | 1150 227 | 750 228 | text-x-generic 229 | com.github.bvinc.gxi 230 | 231 | 232 | True 233 | False 234 | gxi 235 | True 236 | 237 | 238 | Open 239 | True 240 | True 241 | True 242 | app.open 243 | 244 | 245 | 246 | 247 | True 248 | True 249 | True 250 | app.new 251 | new_tab_image 252 | True 253 | 254 | 255 | 256 | 1 257 | 258 | 259 | 260 | 261 | True 262 | True 263 | True 264 | hamburger_popover 265 | 266 | 267 | True 268 | False 269 | open-menu-symbolic 270 | 271 | 272 | 273 | 274 | end 275 | 1 276 | 277 | 278 | 279 | 280 | Save 281 | True 282 | True 283 | True 284 | app.save 285 | 286 | 287 | 288 | end 289 | 3 290 | 291 | 292 | 293 | 294 | 295 | 296 | True 297 | True 298 | True 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | False 322 | center 323 | dialog 324 | appwindow 325 | 326 | 327 | 328 | 329 | 330 | False 331 | vertical 332 | 2 333 | 334 | 335 | False 336 | end 337 | 338 | 339 | Close without Saving 340 | True 341 | True 342 | True 343 | 344 | 345 | True 346 | True 347 | 0 348 | 349 | 350 | 351 | 352 | Cancel 353 | True 354 | True 355 | True 356 | 357 | 358 | True 359 | True 360 | 1 361 | 362 | 363 | 364 | 365 | Save As 366 | True 367 | True 368 | True 369 | 370 | 371 | True 372 | True 373 | 2 374 | 375 | 376 | 377 | 378 | False 379 | False 380 | 0 381 | 382 | 383 | 384 | 385 | True 386 | False 387 | 18 388 | 18 389 | 18 390 | 18 391 | 16 392 | 393 | 394 | True 395 | False 396 | gtk-dialog-question 397 | 6 398 | 399 | 400 | False 401 | True 402 | 0 403 | 404 | 405 | 406 | 407 | True 408 | False 409 | vertical 410 | 411 | 412 | True 413 | False 414 | Save changes to document "Untitled" before closing? 415 | 416 | 417 | 418 | 419 | 420 | True 421 | True 422 | 0 423 | 424 | 425 | 426 | 427 | False 428 | True 429 | 1 430 | 431 | 432 | 433 | 434 | True 435 | True 436 | 1 437 | 438 | 439 | 440 | 441 | 442 | close_button 443 | button2 444 | button1 445 | 446 | 447 | 448 | -------------------------------------------------------------------------------- /src/ui/prefs_win.glade: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | False 7 | center-on-parent 8 | dialog 9 | 10 | 11 | 12 | 13 | 14 | True 15 | True 16 | 17 | 18 | True 19 | False 20 | 18 21 | 18 22 | 18 23 | 18 24 | 6 25 | 12 26 | 27 | 28 | True 29 | False 30 | end 31 | Font 32 | right 33 | 34 | 35 | 0 36 | 0 37 | 38 | 39 | 40 | 41 | True 42 | False 43 | end 44 | Theme 45 | 46 | 47 | 0 48 | 1 49 | 50 | 51 | 52 | 53 | True 54 | False 55 | True 56 | 57 | 58 | 1 59 | 0 60 | 61 | 62 | 63 | 64 | True 65 | False 66 | True 67 | 68 | 69 | 1 70 | 1 71 | 72 | 73 | 74 | 75 | 76 | 77 | True 78 | False 79 | Font & Theme 80 | 81 | 82 | False 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /src/xi_thread.rs: -------------------------------------------------------------------------------- 1 | use std::io::{self, BufRead, ErrorKind, Read, Write}; 2 | use std::sync::mpsc::{channel, Receiver, Sender}; 3 | use std::thread; 4 | #[allow(unused_imports)] 5 | use std::time::Duration; 6 | 7 | use serde_json::{self, Value}; 8 | 9 | use xi_core_lib::XiCore; 10 | use xi_rpc::RpcLoop; 11 | 12 | pub struct XiPeer { 13 | tx: Sender, 14 | } 15 | 16 | impl XiPeer { 17 | pub fn send(&self, s: String) { 18 | let _ = self.tx.send(s); 19 | } 20 | 21 | pub fn send_json(&self, v: &Value) { 22 | self.send(serde_json::to_string(v).unwrap()); 23 | } 24 | } 25 | 26 | pub fn start_xi_thread() -> (XiPeer, Receiver) { 27 | let (to_core_tx, to_core_rx) = channel(); 28 | let to_core_rx = ChanReader(to_core_rx); 29 | let (from_core_tx, from_core_rx) = channel(); 30 | let from_core_tx = ChanWriter { 31 | sender: from_core_tx, 32 | }; 33 | let mut state = XiCore::new(); 34 | let mut rpc_looper = RpcLoop::new(from_core_tx); 35 | thread::spawn(move || rpc_looper.mainloop(|| to_core_rx, &mut state)); 36 | let peer = XiPeer { tx: to_core_tx }; 37 | (peer, from_core_rx) 38 | } 39 | 40 | struct ChanReader(Receiver); 41 | 42 | impl Read for ChanReader { 43 | fn read(&mut self, _buf: &mut [u8]) -> io::Result { 44 | unreachable!("didn't expect xi-rpc to call read"); 45 | } 46 | } 47 | 48 | // Note: we don't properly implement BufRead, only the stylized call patterns 49 | // used by xi-rpc. 50 | impl BufRead for ChanReader { 51 | fn fill_buf(&mut self) -> io::Result<&[u8]> { 52 | unreachable!("didn't expect xi-rpc to call fill_buf"); 53 | } 54 | 55 | fn consume(&mut self, _amt: usize) { 56 | unreachable!("didn't expect xi-rpc to call consume"); 57 | } 58 | 59 | fn read_line(&mut self, buf: &mut String) -> io::Result { 60 | match self.0.recv() { 61 | Ok(s) => { 62 | buf.push_str(&s); 63 | Ok(s.len()) 64 | } 65 | Err(_) => Ok(0), 66 | } 67 | } 68 | } 69 | 70 | struct ChanWriter { 71 | sender: Sender, 72 | } 73 | 74 | impl Write for ChanWriter { 75 | fn write(&mut self, _buf: &[u8]) -> io::Result { 76 | unreachable!("didn't expect xi-rpc to call write"); 77 | } 78 | 79 | fn flush(&mut self) -> io::Result<()> { 80 | unreachable!("didn't expect xi-rpc to call flush"); 81 | } 82 | 83 | fn write_all(&mut self, buf: &[u8]) -> io::Result<()> { 84 | let json = serde_json::from_slice::(buf).unwrap(); 85 | self.sender 86 | .send(json) 87 | .map_err(|_| io::Error::new(ErrorKind::BrokenPipe, "rpc rx thread lost")) 88 | } 89 | } 90 | --------------------------------------------------------------------------------