diff --git a/defaults/settings.toml b/defaults/settings.toml index 8886bf44..997dc9dc 100644 --- a/defaults/settings.toml +++ b/defaults/settings.toml @@ -32,6 +32,7 @@ status-height = 25 tab-min-width = 100 scroll-width = 10 drop-shadow-width = 0 +custom-titlebar = false [theme] name = "" diff --git a/lapce-data/src/config.rs b/lapce-data/src/config.rs index 0d13e81e..ce036756 100644 --- a/lapce-data/src/config.rs +++ b/lapce-data/src/config.rs @@ -225,6 +225,11 @@ pub struct UIConfig { #[field_names(desc = "Controls the width of drop shadow in the UI")] drop_shadow_width: usize, + + #[field_names( + desc = "Enable customised titlebar and disable OS native one (Windows only)" + )] + custom_titlebar: bool, } impl UIConfig { @@ -261,6 +266,10 @@ pub fn scroll_width(&self) -> usize { pub fn drop_shadow_width(&self) -> usize { self.drop_shadow_width } + + pub fn custom_titlebar(&self) -> bool { + self.custom_titlebar + } } #[derive(FieldNames, Debug, Clone, Deserialize, Serialize, Default)] diff --git a/lapce-ui/src/app.rs b/lapce-ui/src/app.rs index 4d27914d..52e26c57 100644 --- a/lapce-ui/src/app.rs +++ b/lapce-ui/src/app.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + use druid::{ AppDelegate, AppLauncher, Command, Env, Event, LocalizedString, Point, Size, Widget, WidgetExt, WindowDesc, WindowHandle, WindowId, WindowState, @@ -61,6 +63,7 @@ pub fn launch() { window_data.size, window_data.pos, window_data.maximised, + &window_data.config, ); launcher = launcher.with_window(window); } @@ -75,6 +78,8 @@ fn new_window_desc( size: Size, pos: Point, maximised: bool, + #[cfg(target_os = "windows")] config: &Arc, + #[cfg(not(target_os = "windows"))] _config: &Arc, ) -> WindowDesc where W: Widget + 'static, @@ -83,6 +88,12 @@ fn new_window_desc( .title(LocalizedString::new("Lapce").with_placeholder("Lapce")) .window_size(size) .set_position(pos); + + #[cfg(target_os = "windows")] + if config.ui.custom_titlebar() { + desc = desc.show_titlebar(false); + } + if maximised { desc = desc.set_window_state(WindowState::Maximized); } @@ -227,13 +238,14 @@ fn command( ); let root = build_window(&mut window_data); let window_id = window_data.window_id; - data.windows.insert(window_id, window_data); + data.windows.insert(window_id, window_data.clone()); let desc = new_window_desc( window_id, root, info.size, info.pos, info.maximised, + &window_data.config, ); ctx.new_window(desc); return druid::Handled::Yes; diff --git a/lapce-ui/src/title.rs b/lapce-ui/src/title.rs index ac7e9226..7c01009a 100644 --- a/lapce-ui/src/title.rs +++ b/lapce-ui/src/title.rs @@ -8,7 +8,7 @@ LifeCycleCtx, MouseEvent, PaintCtx, Point, Rect, RenderContext, Size, Target, UpdateCtx, Widget, }; -#[cfg(target_os = "macos")] +#[cfg(any(target_os = "macos", target_os = "windows"))] use druid::{WindowConfig, WindowState}; use lapce_data::{ command::{ @@ -64,8 +64,10 @@ fn event( &mut self, ctx: &mut EventCtx, event: &Event, - #[cfg(target_os = "macos")] data: &mut LapceWindowData, - #[cfg(not(target_os = "macos"))] _data: &mut LapceWindowData, + #[cfg(any(target_os = "macos", target_os = "windows"))] + data: &mut LapceWindowData, + #[cfg(not(any(target_os = "macos", target_os = "windows")))] + _data: &mut LapceWindowData, _env: &Env, ) { match event { @@ -77,14 +79,20 @@ fn event( } else { ctx.clear_cursor(); ctx.request_paint(); + + #[cfg(target_os = "windows")] + // ! Currently implemented on Windows only + ctx.window().handle_titlebar(true); } } Event::MouseDown(mouse_event) => { self.mouse_down(ctx, mouse_event); } - #[cfg(target_os = "macos")] + #[cfg(any(target_os = "macos", target_os = "windows"))] Event::MouseUp(mouse_event) => { - if mouse_event.count >= 2 { + if (cfg!(target_os = "macos") || data.config.ui.custom_titlebar()) + && mouse_event.count >= 2 + { let state = match ctx.window().get_window_state() { WindowState::Maximized => WindowState::Restored, WindowState::Restored => WindowState::Maximized, @@ -112,11 +120,25 @@ fn lifecycle( fn update( &mut self, - _ctx: &mut UpdateCtx, - _old_data: &LapceWindowData, - _data: &LapceWindowData, + #[cfg(target_os = "windows")] ctx: &mut UpdateCtx, + #[cfg(not(target_os = "windows"))] _ctx: &mut UpdateCtx, + #[cfg(target_os = "windows")] old_data: &LapceWindowData, + #[cfg(not(target_os = "windows"))] _old_data: &LapceWindowData, + #[cfg(target_os = "windows")] data: &LapceWindowData, + #[cfg(not(target_os = "windows"))] _data: &LapceWindowData, _env: &Env, ) { + #[cfg(target_os = "windows")] + if old_data.config.ui.custom_titlebar() != data.config.ui.custom_titlebar() { + ctx.submit_command( + druid::commands::CONFIGURE_WINDOW + .with( + WindowConfig::default() + .show_titlebar(!data.config.ui.custom_titlebar()), + ) + .to(Target::Window(data.window_id)), + ) + } } fn layout( @@ -126,7 +148,15 @@ fn layout( _data: &LapceWindowData, _env: &Env, ) -> Size { - Size::new(bc.max().width, 28.0) + #[cfg(not(target_os = "windows"))] + { + Size::new(bc.max().width, 28.0) + } + + #[cfg(target_os = "windows")] + { + Size::new(bc.max().width, 32.0) + } } fn paint(&mut self, ctx: &mut PaintCtx, data: &LapceWindowData, _env: &Env) { @@ -147,6 +177,27 @@ fn paint(&mut self, ctx: &mut PaintCtx, data: &LapceWindowData, _env: &Env) { let padding = 15.0; + #[cfg(target_os = "windows")] + { + let logo_rect = Size::new(size.height, size.height) + .to_rect() + .with_origin(Point::new(x, 0.0)); + let logo_svg = crate::svg::logo_svg(); + ctx.draw_svg( + &logo_svg, + logo_rect.inflate(-5.0, -5.0), + Some( + &data + .config + .get_color_unchecked(LapceTheme::EDITOR_DIM) + .clone() + .with_alpha(0.5), + ), + ); + + x += size.height; + } + let command_rect = Size::ZERO.to_rect().with_origin(Point::new(x, 0.0)); let tab = data.tabs.get(&data.active_id).unwrap(); let remote_text = match &tab.workspace.kind { @@ -448,8 +499,39 @@ fn paint(&mut self, ctx: &mut PaintCtx, data: &LapceWindowData, _env: &Env) { ctx.stroke(line, line_color, 1.0); } - x = size.width; - x -= size.height; + #[cfg(target_os = "windows")] + { + let title_layout = ctx + .text() + .new_text_layout(String::from("Lapce")) + .font( + data.config.ui.font_family(), + data.config.ui.font_size() as f64, + ) + .text_color( + data.config + .get_color_unchecked(LapceTheme::EDITOR_FOREGROUND) + .clone(), + ) + .build() + .unwrap(); + ctx.draw_text( + &title_layout, + Point::new( + (size.width - title_layout.size().width) / 2.0, + (size.height - title_layout.size().height) / 2.0, + ), + ); + + if data.config.ui.custom_titlebar() { + x = size.width - (size.height * 4.0); + } + } + + if cfg!(not(target_os = "windows")) || !data.config.ui.custom_titlebar() { + x = size.width - size.height; + } + let settings_rect = Size::new(size.height, size.height) .to_rect() .with_origin(Point::new(x, 0.0)); @@ -505,5 +587,155 @@ fn paint(&mut self, ctx: &mut PaintCtx, data: &LapceWindowData, _env: &Env) { Target::Auto, ), )); + + #[cfg(target_os = "windows")] + if data.config.ui.custom_titlebar() { + let font_size = 10.0; + let font_family = "Segoe MDL2 Assets"; + + #[derive(strum_macros::Display)] + enum WindowControls { + Minimise, + Maximise, + Restore, + Close, + } + + impl WindowControls { + fn as_str(&self) -> &'static str { + match self { + WindowControls::Minimise => "\u{E949}", + WindowControls::Maximise => "\u{E739}", + WindowControls::Restore => "\u{E923}", + WindowControls::Close => "\u{E106}", + } + } + } + + x += size.height; + let minimise_text = ctx + .text() + .new_text_layout(WindowControls::Minimise.as_str()) + .font(ctx.text().font_family(font_family).unwrap(), font_size) + .text_color( + data.config + .get_color_unchecked(LapceTheme::EDITOR_FOREGROUND) + .clone(), + ) + .build() + .unwrap(); + ctx.draw_text( + &minimise_text, + Point::new( + x + ((minimise_text.size().width + 5.0) / 2.0), + (size.height - minimise_text.size().height) / 2.0, + ), + ); + let minimise_rect = Size::new( + size.height + + Some(minimise_text) + .as_ref() + .map(|t| t.size().width.round() + padding - 5.0) + .unwrap_or(0.0), + size.height, + ) + .to_rect() + .with_origin(Point::new(x, 0.0)); + + self.commands.push(( + minimise_rect, + Command::new( + druid::commands::CONFIGURE_WINDOW, + WindowConfig::default().set_window_state(WindowState::Minimized), + Target::Window(data.window_id), + ), + )); + + x += size.height; + + let max_res_icon; + let max_res_state; + + if ctx.window().get_window_state() == WindowState::Restored { + max_res_icon = WindowControls::Maximise; + max_res_state = WindowState::Maximized; + } else { + max_res_icon = WindowControls::Restore; + max_res_state = WindowState::Restored; + }; + + let max_res_text = ctx + .text() + .new_text_layout(max_res_icon.as_str()) + .font(ctx.text().font_family(font_family).unwrap(), font_size) + .text_color( + data.config + .get_color_unchecked(LapceTheme::EDITOR_FOREGROUND) + .clone(), + ) + .build() + .unwrap(); + ctx.draw_text( + &max_res_text, + Point::new( + x + ((max_res_text.size().width + 5.0) / 2.0), + (size.height - max_res_text.size().height) / 2.0, + ), + ); + + let max_res_rect = Size::new( + size.height + + Some(max_res_text) + .as_ref() + .map(|t| t.size().width.round() + padding - 5.0) + .unwrap_or(0.0), + size.height, + ) + .to_rect() + .with_origin(Point::new(x, 0.0)); + self.commands.push(( + max_res_rect, + Command::new( + druid::commands::CONFIGURE_WINDOW, + WindowConfig::default().set_window_state(max_res_state), + Target::Window(data.window_id), + ), + )); + + x += size.height; + let close_text = ctx + .text() + .new_text_layout(WindowControls::Close.as_str()) + .font(ctx.text().font_family(font_family).unwrap(), font_size) + .text_color( + data.config + .get_color_unchecked(LapceTheme::EDITOR_FOREGROUND) + .clone(), + ) + .build() + .unwrap(); + ctx.draw_text( + &close_text, + Point::new( + x + ((close_text.size().width + 5.0) / 2.0), + (size.height - close_text.size().height) / 2.0, + ), + ); + let close_rect = Size::new( + size.height + + Some(close_text) + .as_ref() + .map(|t| t.size().width.round() + padding + 5.0) + .unwrap_or(0.0), + size.height, + ) + .to_rect() + .with_origin(Point::new(x, 0.0)); + + self.commands.push(( + close_rect, + Command::new(druid::commands::QUIT_APP, (), Target::Global), + )); + } } }