nvim_gtk/
ui.rs

1use std::cell::{Ref, RefCell, RefMut};
2use std::path::Path;
3use std::rc::Rc;
4use std::sync::Arc;
5use std::{env, thread};
6
7use gdk;
8use gio::prelude::*;
9use gio::{Menu, MenuItem, SimpleAction};
10use glib::variant::FromVariant;
11use gtk;
12use gtk::prelude::*;
13use gtk::{AboutDialog, ApplicationWindow, Button, HeaderBar, Orientation, Paned, SettingsExt};
14
15use toml;
16
17use neovim_lib::NeovimApi;
18
19use crate::file_browser::FileBrowserWidget;
20use crate::misc;
21use crate::nvim::{ErrorReport, NvimCommand};
22use crate::plug_manager;
23use crate::project::Projects;
24use crate::settings::{Settings, SettingsLoader};
25use crate::shell::{self, Shell, ShellOptions};
26use crate::shell_dlg;
27use crate::subscriptions::{SubscriptionHandle, SubscriptionKey};
28
29macro_rules! clone {
30    (@param _) => ( _ );
31    (@param $x:ident) => ( $x );
32    ($($n:ident),+ => move || $body:expr) => (
33        {
34            $( let $n = $n.clone(); )+
35                move || $body
36        }
37    );
38    ($($n:ident),+ => move |$($p:tt),+| $body:expr) => (
39        {
40            $( let $n = $n.clone(); )+
41                move |$(clone!(@param $p),)+| $body
42        }
43    );
44}
45
46const DEFAULT_WIDTH: i32 = 800;
47const DEFAULT_HEIGHT: i32 = 600;
48const DEFAULT_SIDEBAR_WIDTH: i32 = 200;
49
50pub struct Ui {
51    open_paths: Box<[String]>,
52    initialized: bool,
53    comps: Arc<UiMutex<Components>>,
54    settings: Rc<RefCell<Settings>>,
55    shell: Rc<RefCell<Shell>>,
56    projects: Arc<UiMutex<Projects>>,
57    plug_manager: Arc<UiMutex<plug_manager::Manager>>,
58    file_browser: Arc<UiMutex<FileBrowserWidget>>,
59}
60
61pub struct Components {
62    window: Option<ApplicationWindow>,
63    window_state: WindowState,
64    open_btn: Button,
65}
66
67impl Components {
68    fn new() -> Components {
69        let open_btn = Button::new();
70        let open_btn_box = gtk::Box::new(gtk::Orientation::Horizontal, 3);
71        open_btn_box.pack_start(&gtk::Label::new(Some("Open")), false, false, 3);
72        open_btn_box.pack_start(
73            &gtk::Image::new_from_icon_name(Some("pan-down-symbolic"), gtk::IconSize::Menu),
74            false,
75            false,
76            3,
77        );
78        open_btn.add(&open_btn_box);
79        open_btn.set_can_focus(false);
80        Components {
81            open_btn,
82            window: None,
83            window_state: WindowState::load(),
84        }
85    }
86
87    pub fn close_window(&self) {
88        self.window.as_ref().unwrap().destroy();
89    }
90
91    pub fn window(&self) -> &ApplicationWindow {
92        self.window.as_ref().unwrap()
93    }
94}
95
96impl Ui {
97    pub fn new(options: ShellOptions, open_paths: Box<[String]>) -> Ui {
98        let plug_manager = plug_manager::Manager::new();
99
100        let plug_manager = Arc::new(UiMutex::new(plug_manager));
101        let file_browser = Arc::new(UiMutex::new(FileBrowserWidget::new()));
102        let comps = Arc::new(UiMutex::new(Components::new()));
103        let settings = Rc::new(RefCell::new(Settings::new()));
104        let shell = Rc::new(RefCell::new(Shell::new(settings.clone(), options)));
105        settings.borrow_mut().set_shell(Rc::downgrade(&shell));
106
107        let projects = Projects::new(&comps.borrow().open_btn, shell.clone());
108
109        Ui {
110            initialized: false,
111            comps,
112            shell,
113            settings,
114            projects,
115            plug_manager,
116            file_browser,
117            open_paths,
118        }
119    }
120
121    pub fn init(&mut self, app: &gtk::Application, restore_win_state: bool) {
122        if self.initialized {
123            return;
124        }
125        self.initialized = true;
126
127        let mut settings = self.settings.borrow_mut();
128        settings.init();
129
130        let window = ApplicationWindow::new(app);
131
132        let main = Paned::new(Orientation::Horizontal);
133
134        {
135            // initialize window from comps
136            // borrowing of comps must be leaved
137            // for event processing
138            let mut comps = self.comps.borrow_mut();
139
140            self.shell.borrow_mut().init();
141
142            comps.window = Some(window.clone());
143
144            let prefer_dark_theme = env::var("NVIM_GTK_PREFER_DARK_THEME")
145                .map(|opt| opt.trim() == "1")
146                .unwrap_or(false);
147            if prefer_dark_theme {
148                if let Some(settings) = window.get_settings() {
149                    settings.set_property_gtk_application_prefer_dark_theme(true);
150                }
151            }
152
153            if restore_win_state {
154                if comps.window_state.is_maximized {
155                    window.maximize();
156                }
157
158                window.set_default_size(
159                    comps.window_state.current_width,
160                    comps.window_state.current_height,
161                );
162
163                main.set_position(comps.window_state.sidebar_width);
164            } else {
165                window.set_default_size(DEFAULT_WIDTH, DEFAULT_HEIGHT);
166                main.set_position(DEFAULT_SIDEBAR_WIDTH);
167            }
168        }
169
170        // Client side decorations including the toolbar are disabled via NVIM_GTK_NO_HEADERBAR=1
171        let use_header_bar = env::var("NVIM_GTK_NO_HEADERBAR")
172            .map(|opt| opt.trim() != "1")
173            .unwrap_or(true);
174
175        let disable_window_decoration = env::var("NVIM_GTK_NO_WINDOW_DECORATION")
176            .map(|opt| opt.trim() == "1")
177            .unwrap_or(false);
178
179        if disable_window_decoration {
180            window.set_decorated(false);
181        }
182
183        let update_subtitle = if use_header_bar {
184            Some(self.create_header_bar(app))
185        } else {
186            None
187        };
188
189        let show_sidebar_action =
190            SimpleAction::new_stateful("show-sidebar", None, &false.to_variant());
191        let file_browser_ref = self.file_browser.clone();
192        let comps_ref = self.comps.clone();
193        show_sidebar_action.connect_change_state(move |action, value| {
194            if let Some(value) = value {
195                action.set_state(value);
196                let is_active = value.get::<bool>().unwrap();
197                file_browser_ref.borrow().set_visible(is_active);
198                comps_ref.borrow_mut().window_state.show_sidebar = is_active;
199            }
200        });
201        app.add_action(&show_sidebar_action);
202
203        let comps_ref = self.comps.clone();
204        window.connect_size_allocate(clone!(main => move |window, _| {
205            gtk_window_size_allocate(
206                window,
207                &mut *comps_ref.borrow_mut(),
208                &main,
209            );
210        }));
211
212        let comps_ref = self.comps.clone();
213        window.connect_window_state_event(move |_, event| {
214            gtk_window_state_event(event, &mut *comps_ref.borrow_mut());
215            Inhibit(false)
216        });
217
218        let comps_ref = self.comps.clone();
219        window.connect_destroy(move |_| {
220            comps_ref.borrow().window_state.save();
221        });
222
223        let shell = self.shell.borrow();
224        let file_browser = self.file_browser.borrow();
225        main.pack1(&**file_browser, false, false);
226        main.pack2(&**shell, true, false);
227
228        window.add(&main);
229
230        window.show_all();
231
232        if restore_win_state {
233            // Hide sidebar, if it wasn't shown last time.
234            // Has to be done after show_all(), so it won't be shown again.
235            let show_sidebar = self.comps.borrow().window_state.show_sidebar;
236            show_sidebar_action.change_state(&show_sidebar.to_variant());
237        }
238
239        let comps_ref = self.comps.clone();
240        let update_title = shell.state.borrow().subscribe(
241            SubscriptionKey::from("BufEnter,DirChanged"),
242            &["expand('%:p')", "getcwd()"],
243            move |args| update_window_title(&comps_ref, args),
244        );
245
246        let shell_ref = self.shell.clone();
247        let update_completeopt = shell.state.borrow().subscribe(
248            SubscriptionKey::with_pattern("OptionSet", "completeopt"),
249            &["&completeopt"],
250            move |args| set_completeopts(&*shell_ref, args),
251        );
252
253        let comps_ref = self.comps.clone();
254        let shell_ref = self.shell.clone();
255        window.connect_delete_event(move |_, _| gtk_delete(&*comps_ref, &*shell_ref));
256
257        shell.grab_focus();
258
259        let comps_ref = self.comps.clone();
260        shell.set_detach_cb(Some(move || {
261            let comps_ref = comps_ref.clone();
262            gtk::idle_add(move || {
263                comps_ref.borrow().close_window();
264                Continue(false)
265            });
266        }));
267
268        let state_ref = self.shell.borrow().state.clone();
269        let file_browser_ref = self.file_browser.clone();
270        let plug_manager_ref = self.plug_manager.clone();
271        let files_list = self.open_paths.clone();
272
273        shell.set_nvim_started_cb(Some(move || {
274            Ui::nvim_started(
275                &state_ref.borrow(),
276                &plug_manager_ref,
277                &file_browser_ref,
278                &files_list,
279                &update_title,
280                &update_subtitle,
281                &update_completeopt,
282            );
283        }));
284
285        let sidebar_action = UiMutex::new(show_sidebar_action);
286        let comps_ref = self.comps.clone();
287        let projects = self.projects.clone();
288        shell.set_nvim_command_cb(Some(
289            move |shell: &mut shell::State, command: NvimCommand| {
290                Ui::nvim_command(shell, command, &sidebar_action, &projects, &comps_ref);
291            },
292        ));
293    }
294
295    fn nvim_started(
296        shell: &shell::State,
297        plug_manager: &UiMutex<plug_manager::Manager>,
298        file_browser: &UiMutex<FileBrowserWidget>,
299        files_list: &Box<[String]>,
300        update_title: &SubscriptionHandle,
301        update_subtitle: &Option<SubscriptionHandle>,
302        update_completeopt: &SubscriptionHandle,
303    ) {
304        plug_manager
305            .borrow_mut()
306            .init_nvim_client(shell.nvim_clone());
307        file_browser.borrow_mut().init(shell);
308        shell.set_autocmds();
309        shell.run_now(&update_title);
310        shell.run_now(&update_completeopt);
311        if let Some(ref update_subtitle) = update_subtitle {
312            shell.run_now(&update_subtitle);
313        }
314
315        // open files as last command
316        // because it can generate user query
317        if !files_list.is_empty() {
318            let command = files_list
319                .iter()
320                .fold(":ar".to_owned(), |command, filename| {
321                    let filename = misc::escape_filename(filename);
322                    command + " " + &filename
323                });
324            shell.nvim().unwrap().command(&command).report_err();
325        }
326    }
327
328    fn nvim_command(
329        shell: &mut shell::State,
330        command: NvimCommand,
331        sidebar_action: &UiMutex<SimpleAction>,
332        projects: &Arc<UiMutex<Projects>>,
333        comps: &UiMutex<Components>,
334    ) {
335        match command {
336            NvimCommand::ShowProjectView => {
337                gtk::idle_add(clone!(projects => move || {
338                    projects.borrow_mut().show();
339                    Continue(false)
340                }));
341            }
342            NvimCommand::ToggleSidebar => {
343                let action = sidebar_action.borrow();
344                let state = !bool::from_variant(&action.get_state().unwrap()).unwrap();
345                action.change_state(&state.to_variant());
346            }
347            NvimCommand::Transparency(background_alpha, filled_alpha) => {
348                let comps = comps.borrow();
349                let window = comps.window.as_ref().unwrap();
350
351                let screen = window.get_screen().unwrap();
352                if screen.is_composited() {
353                    let enabled = shell.set_transparency(background_alpha, filled_alpha);
354                    window.set_app_paintable(enabled);
355                } else {
356                    warn!("Screen is not composited");
357                }
358            }
359            NvimCommand::PreferDarkTheme(prefer_dark_theme) => {
360                let comps = comps.borrow();
361                let window = comps.window.as_ref().unwrap();
362
363                if let Some(settings) = window.get_settings() {
364                    settings.set_property_gtk_application_prefer_dark_theme(prefer_dark_theme);
365                }
366            }
367        }
368    }
369
370    fn create_header_bar(&self, app: &gtk::Application) -> SubscriptionHandle {
371        let header_bar = HeaderBar::new();
372        let comps = self.comps.borrow();
373        let window = comps.window.as_ref().unwrap();
374
375        let projects = self.projects.clone();
376        header_bar.pack_start(&comps.open_btn);
377        comps
378            .open_btn
379            .connect_clicked(move |_| projects.borrow_mut().show());
380
381        let new_tab_btn =
382            Button::new_from_icon_name(Some("tab-new-symbolic"), gtk::IconSize::SmallToolbar);
383        let shell_ref = Rc::clone(&self.shell);
384        new_tab_btn.connect_clicked(move |_| shell_ref.borrow_mut().new_tab());
385        new_tab_btn.set_can_focus(false);
386        new_tab_btn.set_tooltip_text(Some("Open a new tab"));
387        header_bar.pack_start(&new_tab_btn);
388
389        header_bar.pack_end(&self.create_primary_menu_btn(app, &window));
390
391        let paste_btn =
392            Button::new_from_icon_name(Some("edit-paste-symbolic"), gtk::IconSize::SmallToolbar);
393        let shell = self.shell.clone();
394        paste_btn.connect_clicked(move |_| shell.borrow_mut().edit_paste());
395        paste_btn.set_can_focus(false);
396        paste_btn.set_tooltip_text(Some("Paste from clipboard"));
397        header_bar.pack_end(&paste_btn);
398
399        let save_btn = Button::new_with_label("Save All");
400        let shell = self.shell.clone();
401        save_btn.connect_clicked(move |_| shell.borrow_mut().edit_save_all());
402        save_btn.set_can_focus(false);
403        header_bar.pack_end(&save_btn);
404
405        header_bar.set_show_close_button(true);
406
407        window.set_titlebar(Some(&header_bar));
408
409        let shell = self.shell.borrow();
410
411        let update_subtitle = shell.state.borrow().subscribe(
412            SubscriptionKey::from("DirChanged"),
413            &["getcwd()"],
414            move |args| {
415                header_bar.set_subtitle(Some(&*args[0]));
416            },
417        );
418
419        update_subtitle
420    }
421
422    fn create_primary_menu_btn(
423        &self,
424        app: &gtk::Application,
425        window: &gtk::ApplicationWindow,
426    ) -> gtk::MenuButton {
427        let plug_manager = self.plug_manager.clone();
428        let btn = gtk::MenuButton::new();
429        btn.set_can_focus(false);
430        btn.set_image(Some(&gtk::Image::new_from_icon_name(
431            Some("open-menu-symbolic"),
432            gtk::IconSize::SmallToolbar,
433        )));
434
435        // note actions created in application menu
436        let menu = Menu::new();
437
438        let section = Menu::new();
439        section.append_item(&MenuItem::new(Some("New Window"), Some("app.new-window")));
440        menu.append_section(None, &section);
441
442        let section = Menu::new();
443        section.append_item(&MenuItem::new(Some("Sidebar"), Some("app.show-sidebar")));
444        menu.append_section(None, &section);
445
446        let section = Menu::new();
447        section.append_item(&MenuItem::new(Some("Plugins"), Some("app.Plugins")));
448        section.append_item(&MenuItem::new(Some("About"), Some("app.HelpAbout")));
449        menu.append_section(None, &section);
450
451        menu.freeze();
452
453        let plugs_action = SimpleAction::new("Plugins", None);
454        plugs_action.connect_activate(
455            clone!(window => move |_, _| plug_manager::Ui::new(&plug_manager).show(&window)),
456        );
457
458        let about_action = SimpleAction::new("HelpAbout", None);
459        about_action.connect_activate(clone!(window => move |_, _| on_help_about(&window)));
460        about_action.set_enabled(true);
461
462        app.add_action(&about_action);
463        app.add_action(&plugs_action);
464
465        btn.set_menu_model(Some(&menu));
466        btn
467    }
468}
469
470fn on_help_about(window: &gtk::ApplicationWindow) {
471    let about = AboutDialog::new();
472    about.set_transient_for(Some(window));
473    about.set_program_name("NeovimGtk");
474    about.set_version(Some(crate::GIT_BUILD_VERSION.unwrap_or(env!("CARGO_PKG_VERSION"))));
475    about.set_logo_icon_name(Some("org.daa.NeovimGtk"));
476    about.set_authors(&[env!("CARGO_PKG_AUTHORS")]);
477    about.set_comments(Some(misc::about_comments().as_str()));
478
479    about.connect_response(|about, _| about.destroy());
480    about.show();
481}
482
483fn gtk_delete(comps: &UiMutex<Components>, shell: &RefCell<Shell>) -> Inhibit {
484    if !shell.borrow().is_nvim_initialized() {
485        return Inhibit(false);
486    }
487
488    Inhibit(if shell_dlg::can_close_window(comps, shell) {
489        let comps = comps.borrow();
490        comps.close_window();
491        shell.borrow_mut().detach_ui();
492        false
493    } else {
494        true
495    })
496}
497
498fn gtk_window_size_allocate(
499    app_window: &gtk::ApplicationWindow,
500    comps: &mut Components,
501    main: &Paned,
502) {
503    if !app_window.is_maximized() {
504        let (current_width, current_height) = app_window.get_size();
505        comps.window_state.current_width = current_width;
506        comps.window_state.current_height = current_height;
507    }
508    if comps.window_state.show_sidebar {
509        comps.window_state.sidebar_width = main.get_position();
510    }
511}
512
513fn gtk_window_state_event(event: &gdk::EventWindowState, comps: &mut Components) {
514    comps.window_state.is_maximized = event
515        .get_new_window_state()
516        .contains(gdk::WindowState::MAXIMIZED);
517}
518
519fn set_completeopts(shell: &RefCell<Shell>, args: Vec<String>) {
520    let options = &args[0];
521
522    shell.borrow().set_completeopts(options);
523}
524
525fn update_window_title(comps: &Arc<UiMutex<Components>>, args: Vec<String>) {
526    let comps_ref = comps.clone();
527    let comps = comps_ref.borrow();
528    let window = comps.window.as_ref().unwrap();
529
530    let file_path = &args[0];
531    let dir = Path::new(&args[1]);
532    let filename = if file_path.is_empty() {
533        "[No Name]"
534    } else if let Some(rel_path) = Path::new(&file_path)
535        .strip_prefix(&dir)
536        .ok()
537        .and_then(|p| p.to_str())
538    {
539        rel_path
540    } else {
541        &file_path
542    };
543
544    window.set_title(filename);
545}
546
547#[derive(Serialize, Deserialize)]
548struct WindowState {
549    current_width: i32,
550    current_height: i32,
551    is_maximized: bool,
552    show_sidebar: bool,
553    sidebar_width: i32,
554}
555
556impl Default for WindowState {
557    fn default() -> Self {
558        WindowState {
559            current_width: DEFAULT_WIDTH,
560            current_height: DEFAULT_HEIGHT,
561            is_maximized: false,
562            show_sidebar: false,
563            sidebar_width: DEFAULT_SIDEBAR_WIDTH,
564        }
565    }
566}
567
568impl SettingsLoader for WindowState {
569    const SETTINGS_FILE: &'static str = "window.toml";
570
571    fn from_str(s: &str) -> Result<Self, String> {
572        toml::from_str(&s).map_err(|e| format!("{}", e))
573    }
574}
575
576pub struct UiMutex<T: ?Sized> {
577    thread: thread::ThreadId,
578    data: RefCell<T>,
579}
580
581unsafe impl<T: ?Sized> Send for UiMutex<T> {}
582unsafe impl<T: ?Sized> Sync for UiMutex<T> {}
583
584impl<T> UiMutex<T> {
585    pub fn new(t: T) -> UiMutex<T> {
586        UiMutex {
587            thread: thread::current().id(),
588            data: RefCell::new(t),
589        }
590    }
591}
592
593impl<T> UiMutex<T> {
594    pub fn replace(&self, t: T) -> T {
595        self.assert_ui_thread();
596        self.data.replace(t)
597    }
598}
599
600impl<T: ?Sized> UiMutex<T> {
601    pub fn borrow(&self) -> Ref<T> {
602        self.assert_ui_thread();
603        self.data.borrow()
604    }
605
606    pub fn borrow_mut(&self) -> RefMut<T> {
607        self.assert_ui_thread();
608        self.data.borrow_mut()
609    }
610
611    #[inline]
612    fn assert_ui_thread(&self) {
613        if thread::current().id() != self.thread {
614            panic!("Can access to UI only from main thread");
615        }
616    }
617}