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(>k::Label::new(Some("Open")), false, false, 3);
72 open_btn_box.pack_start(
73 >k::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: >k::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 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 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 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 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: >k::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: >k::Application,
425 window: >k::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(>k::Image::new_from_icon_name(
431 Some("open-menu-symbolic"),
432 gtk::IconSize::SmallToolbar,
433 )));
434
435 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, §ion);
441
442 let section = Menu::new();
443 section.append_item(&MenuItem::new(Some("Sidebar"), Some("app.show-sidebar")));
444 menu.append_section(None, §ion);
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, §ion);
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: >k::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: >k::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}