fix: rework titlebar (#990)

This commit is contained in:
Jakub Panek 2022-08-29 20:04:45 +01:00 committed by GitHub
parent bd8ad59e2a
commit 46f9fca0fa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 281 additions and 135 deletions

View File

@ -2,6 +2,7 @@
modal = false
color-theme = "Lapce Dark"
icon-theme = ""
custom-titlebar = true
[editor]
font-family = "Cascadia Code"
@ -39,7 +40,6 @@ tab-min-width = 100
scroll-width = 10
drop-shadow-width = 0
preview-editor-width = 0
custom-titlebar = false
hover-font-family = ""
hover-font-size = 0

View File

@ -150,6 +150,10 @@ pub struct LapceConfig {
pub modal: bool,
#[field_names(desc = "Set the color theme of Lapce")]
pub color_theme: String,
#[field_names(
desc = "Enable customised titlebar and disable OS native one (Linux, BSD, Windows)"
)]
pub custom_titlebar: bool,
}
#[derive(FieldNames, Debug, Clone, Deserialize, Serialize, Default)]

View File

@ -121,18 +121,23 @@ fn new_window_desc<W, T: druid::Data>(
size: Size,
pos: Point,
maximised: bool,
_config: &Arc<Config>,
config: &Arc<Config>,
) -> WindowDesc<T>
where
W: Widget<T> + 'static,
{
let mut desc = WindowDesc::new_with_id(window_id, root)
.show_titlebar(false)
.title(LocalizedString::new("Lapce").with_placeholder("Lapce"))
.with_min_size(Size::new(384.0, 384.0))
.window_size(size)
.set_position(pos);
if cfg!(not(target_os = "macos")) {
desc = desc.show_titlebar(!config.lapce.custom_titlebar);
} else {
desc = desc.show_titlebar(false);
}
if maximised {
desc = desc.set_window_state(WindowState::Maximized);
}

View File

@ -2176,12 +2176,14 @@ pub struct LapceTabHeader {
pub drag_start: Option<(Point, Point)>,
pub mouse_pos: Point,
close_icon_rect: Rect,
holding_click_rect: Option<Rect>,
}
impl LapceTabHeader {
pub fn new() -> Self {
Self {
close_icon_rect: Rect::ZERO,
holding_click_rect: None,
drag_start: None,
mouse_pos: Point::ZERO,
}
@ -2218,11 +2220,7 @@ fn event(
}
Event::MouseDown(mouse_event) => {
if self.close_icon_rect.contains(mouse_event.pos) {
ctx.submit_command(Command::new(
LAPCE_UI_COMMAND,
LapceUICommand::CloseTabId(data.id),
Target::Auto,
));
self.holding_click_rect = Some(self.close_icon_rect);
} else {
self.drag_start =
Some((ctx.to_window(mouse_event.pos), ctx.window_origin()));
@ -2235,7 +2233,17 @@ fn event(
));
}
}
Event::MouseUp(_mouse_event) => {
Event::MouseUp(mouse_event) => {
if self.close_icon_rect.contains(mouse_event.pos)
&& self.holding_click_rect.eq(&Some(self.close_icon_rect))
{
ctx.submit_command(Command::new(
LAPCE_UI_COMMAND,
LapceUICommand::CloseTabId(data.id),
Target::Auto,
));
}
self.holding_click_rect = None;
ctx.set_active(false);
self.drag_start = None;
}

View File

@ -3,8 +3,8 @@
#[cfg(not(target_os = "macos"))]
use crate::window::window_controls;
use crate::{palette::Palette, svg::get_svg};
use druid::kurbo::Circle;
use druid::WindowConfig;
use druid::{kurbo::Circle, InternalEvent};
use druid::{
kurbo::Line,
piet::{PietText, PietTextLayout, Svg, Text, TextLayout, TextLayoutBuilder},
@ -28,8 +28,10 @@
pub struct Title {
mouse_pos: Point,
commands: Vec<(Rect, Command)>,
svgs: Vec<(Svg, Rect, Option<Color>)>,
menus: Vec<(Rect, Command)>,
window_controls: Vec<(Rect, Command)>,
holding_click_rect: Option<Rect>,
svgs: Vec<(Svg, Rect, Option<Color>, Option<Color>)>,
text_layouts: Vec<(PietTextLayout, Point)>,
borders: Vec<Line>,
rects: Vec<(Rect, Color)>,
@ -43,7 +45,9 @@ pub fn new(data: &LapceTabData) -> Self {
let palette = Palette::new(data);
Self {
mouse_pos: Point::ZERO,
commands: Vec::new(),
menus: Vec::new(),
window_controls: Vec::new(),
holding_click_rect: None,
svgs: Vec::new(),
text_layouts: Vec::new(),
borders: Vec::new(),
@ -63,7 +67,8 @@ fn update_content(
piet_text: &mut PietText,
size: Size,
) -> Rect {
self.commands.clear();
self.menus.clear();
self.window_controls.clear();
self.svgs.clear();
self.text_layouts.clear();
self.borders.clear();
@ -95,12 +100,13 @@ fn update_content(
.clone()
.with_alpha(0.5),
),
None,
));
x += size.height;
}
let padding = 15.0;
x = self.update_remote(data, piet_text, size, padding, x);
x = self.update_remote(data, size, padding, x);
x = self.update_source_control(data, piet_text, size, padding, x);
let mut region = Region::EMPTY;
@ -137,7 +143,6 @@ fn update_content(
fn update_remote(
&mut self,
data: &LapceTabData,
_piet_text: &mut PietText,
size: Size,
_padding: f64,
x: f64,
@ -164,12 +169,13 @@ fn update_remote(
Size::new(size.height, size.height)
.to_rect()
.with_origin(Point::new(x + 5.0, 0.0))
.inflate(-8.0, -8.0),
.inflate(-6.0, -6.0),
Some(
data.config
.get_color_unchecked(LapceTheme::EDITOR_BACKGROUND)
.clone(),
),
None,
));
let x = x + remote_rect.width();
let command_rect =
@ -184,7 +190,8 @@ fn update_remote(
enabled: true,
})];
if cfg!(target_os = "windows") {
#[cfg(target_os = "windows")]
{
menu_items.push(MenuKind::Item(MenuItem {
desc: None,
command: LapceCommand {
@ -208,7 +215,7 @@ fn update_remote(
}));
}
self.commands.push((
self.menus.push((
command_rect,
Command::new(
LAPCE_UI_COMMAND,
@ -244,12 +251,13 @@ fn update_source_control(
.with_origin(Point::new(x, 0.0));
self.svgs.push((
folder_svg,
folder_rect.inflate(-10.5, -10.5),
folder_rect.inflate(-8.5, -8.5),
Some(
data.config
.get_color_unchecked(LapceTheme::EDITOR_FOREGROUND)
.clone(),
),
None,
));
x += size.height;
@ -294,7 +302,7 @@ fn update_source_control(
})
})
.collect();
self.commands.push((
self.menus.push((
command_rect,
Command::new(
LAPCE_UI_COMMAND,
@ -331,7 +339,10 @@ fn update_settings(
x: f64,
) -> f64 {
let mut x = x;
if cfg!(target_os = "macos") || data.multiple_tab {
if cfg!(target_os = "macos")
|| data.multiple_tab
|| !data.config.lapce.custom_titlebar
{
x -= size.height;
} else {
x = size.width - (size.height * 4.0);
@ -339,6 +350,30 @@ fn update_settings(
let offset = x;
let hover_color = {
let (r, g, b, a) = data
.config
.get_color_unchecked(LapceTheme::PANEL_BACKGROUND)
.to_owned()
.as_rgba8();
// TODO: hacky way to detect "lightness" of colour, should be fixed once we have dark/light themes
if r < 128 || g < 128 || b < 128 {
Color::rgba8(
r.saturating_add(25),
g.saturating_add(25),
b.saturating_add(25),
a,
)
} else {
Color::rgba8(
r.saturating_sub(30),
g.saturating_sub(30),
b.saturating_sub(30),
a,
)
}
};
let settings_rect = Size::new(size.height, size.height)
.to_rect()
.with_origin(Point::new(x, 0.0));
@ -351,6 +386,7 @@ fn update_settings(
.get_color_unchecked(LapceTheme::EDITOR_FOREGROUND)
.clone(),
),
Some(hover_color),
));
let latest_version = data
.latest_release
@ -433,7 +469,7 @@ fn update_settings(
));
self.text_layouts.push((text_layout, point));
}
self.commands.push((
self.menus.push((
settings_rect,
Command::new(
LAPCE_UI_COMMAND,
@ -450,22 +486,21 @@ fn update_settings(
));
#[cfg(not(target_os = "macos"))]
if !data.multiple_tab {
if !data.multiple_tab && data.config.lapce.custom_titlebar {
x += size.height;
let (commands, svgs, text_layouts) = window_controls(
let (window_controls, svgs) = window_controls(
data.window_id,
window_state,
piet_text,
x,
size.height,
&data.config,
);
for command in commands {
self.commands.push(command);
for command in window_controls {
self.window_controls.push(command);
}
for (svg, rect) in svgs {
for (svg, rect, color) in svgs {
self.svgs.push((
svg,
rect,
@ -474,12 +509,9 @@ fn update_settings(
.get_color_unchecked(LapceTheme::EDITOR_FOREGROUND)
.clone(),
),
Some(color),
));
}
for text_layout in text_layouts {
self.text_layouts.push(text_layout);
}
}
offset
@ -549,6 +581,7 @@ fn update_folder(
.get_color_unchecked(LapceTheme::EDITOR_FOREGROUND)
.clone(),
),
None,
));
let menu_items = vec![
MenuKind::Item(MenuItem {
@ -581,8 +614,9 @@ fn update_folder(
.get_color_unchecked(LapceTheme::EDITOR_FOREGROUND)
.clone(),
),
None,
));
self.commands.push((
self.menus.push((
command_rect,
Command::new(
LAPCE_UI_COMMAND,
@ -599,7 +633,12 @@ fn update_folder(
}
fn icon_hit_test(&self, mouse_event: &MouseEvent) -> bool {
for (rect, _) in self.commands.iter() {
for (rect, _) in self.menus.iter() {
if rect.contains(mouse_event.pos) {
return true;
}
}
for (rect, _) in self.window_controls.iter() {
if rect.contains(mouse_event.pos) {
return true;
}
@ -607,9 +646,29 @@ fn icon_hit_test(&self, mouse_event: &MouseEvent) -> bool {
false
}
fn mouse_down(&self, ctx: &mut EventCtx, mouse_event: &MouseEvent) {
for (rect, command) in self.commands.iter() {
fn mouse_down(&mut self, ctx: &mut EventCtx, mouse_event: &MouseEvent) {
for (rect, command) in self.menus.iter() {
if rect.contains(mouse_event.pos) {
self.holding_click_rect = Some(*rect);
ctx.submit_command(command.clone());
ctx.set_handled();
return;
}
}
for (rect, _command) in self.window_controls.iter() {
if rect.contains(mouse_event.pos) {
self.holding_click_rect = Some(*rect);
ctx.set_handled();
return;
}
}
}
fn mouse_up(&self, ctx: &mut EventCtx, mouse_event: &MouseEvent) {
for (rect, command) in self.window_controls.iter() {
if rect.contains(mouse_event.pos)
&& self.holding_click_rect.eq(&Some(*rect))
{
ctx.submit_command(command.clone());
ctx.set_handled();
return;
@ -627,6 +686,10 @@ fn event(
env: &Env,
) {
match event {
Event::Internal(InternalEvent::MouseLeave) => {
self.mouse_pos = Point::ZERO;
ctx.request_paint();
}
Event::MouseMove(mouse_event) => {
self.mouse_pos = mouse_event.pos;
if self.icon_hit_test(mouse_event) {
@ -651,7 +714,9 @@ fn event(
}
}
Event::MouseDown(mouse_event) => {
self.mouse_down(ctx, mouse_event);
if mouse_event.button.is_left() {
self.mouse_down(ctx, mouse_event);
}
}
Event::MouseUp(mouse_event) => {
if !data.multiple_tab
@ -673,7 +738,14 @@ fn event(
.with(WindowConfig::default().set_window_state(state))
.to(Target::Window(data.window_id)),
);
return;
}
if mouse_event.button.is_left() {
self.mouse_up(ctx, mouse_event);
}
self.holding_click_rect = None;
}
_ => {}
}
@ -782,8 +854,25 @@ fn paint(&mut self, ctx: &mut PaintCtx, data: &LapceTabData, env: &Env) {
ctx.fill(rect, color);
}
for (svg, rect, color) in self.svgs.iter() {
ctx.draw_svg(svg, *rect, color.as_ref());
for (svg, rect, color, bg_color) in self.svgs.iter() {
let hover_rect = rect.inflate(10.0, 10.0);
if hover_rect.contains(self.mouse_pos)
&& bg_color.is_some()
&& (self.holding_click_rect.is_none()
|| self.holding_click_rect.unwrap().contains(self.mouse_pos))
{
let bg_color = bg_color.to_owned().unwrap();
ctx.fill(hover_rect, &bg_color);
// TODO: hacky way to detect close button, should be fixed once we have dark/light themes
if bg_color.as_rgba8() == (210, 16, 33, 255) {
ctx.draw_svg(svg, *rect, Some(&Color::WHITE));
} else {
ctx.draw_svg(svg, *rect, color.as_ref());
}
} else {
ctx.draw_svg(svg, *rect, color.as_ref());
}
}
for (circle, color) in self.circles.iter() {

View File

@ -1,10 +1,9 @@
use druid::{
kurbo::Line,
piet::{PietText, PietTextLayout, Svg, Text, TextLayout, TextLayoutBuilder},
widget::{LensWrap, WidgetExt},
BoxConstraints, Command, Data, Env, Event, EventCtx, LayoutCtx, LifeCycle,
LifeCycleCtx, PaintCtx, Point, Rect, Region, RenderContext, Size, Target,
Widget, WidgetId, WidgetPod, WindowConfig, WindowId, WindowState,
BoxConstraints, Command, Data, Env, Event, EventCtx, InternalEvent, LayoutCtx,
LifeCycle, LifeCycleCtx, PaintCtx, Point, Rect, Region, RenderContext, Size,
Target, Widget, WidgetId, WidgetPod, WindowConfig, WindowState,
};
use lapce_data::{
command::{LapceUICommand, LAPCE_UI_COMMAND},
@ -14,12 +13,10 @@
use std::cmp::Ordering;
use std::sync::Arc;
use crate::{
svg::get_svg,
tab::{LapceTab, LapceTabHeader},
};
use crate::tab::{LapceTab, LapceTabHeader};
pub struct LapceWindow {
mouse_pos: Point,
// pub title: WidgetPod<LapceWindowData, Box<dyn Widget<LapceWindowData>>>,
pub tabs: Vec<WidgetPod<LapceWindowData, Box<dyn Widget<LapceWindowData>>>>,
tab_headers: Vec<
@ -31,6 +28,8 @@ pub struct LapceWindow {
dragable_area: Region,
tab_header_cmds: Vec<(Rect, Command)>,
mouse_down_cmd: Option<(Rect, Command)>,
#[cfg(not(target_os = "macos"))]
holding_click_rect: Option<Rect>,
}
impl LapceWindow {
@ -54,11 +53,14 @@ pub fn new(data: &mut LapceWindowData) -> Self {
})
.collect();
Self {
mouse_pos: Point::ZERO,
dragable_area: Region::EMPTY,
tabs,
tab_headers,
tab_header_cmds: Vec::new(),
mouse_down_cmd: None,
#[cfg(not(target_os = "macos"))]
holding_click_rect: None,
}
}
@ -202,12 +204,20 @@ fn event(
Target::Widget(data.active_id),
));
}
Event::Internal(InternalEvent::MouseLeave) => {
self.mouse_pos = Point::ZERO;
ctx.request_paint();
}
Event::MouseMove(mouse_event) => {
ctx.clear_cursor();
if data.tabs.len() > 1 && cfg!(not(target_os = "macos")) {
self.mouse_pos = mouse_event.pos;
#[cfg(not(target_os = "macos"))]
if data.tabs.len() > 1 && mouse_event.count < 2 {
for (rect, _) in self.tab_header_cmds.iter() {
if rect.contains(mouse_event.pos) {
ctx.set_cursor(&druid::Cursor::Pointer);
ctx.request_paint();
break;
}
}
@ -224,15 +234,16 @@ fn event(
ctx.window().handle_titlebar(true);
}
}
Event::MouseDown(mouse_event) => {
Event::MouseDown(_mouse_event) => {
self.mouse_down_cmd = None;
if data.tabs.len() > 1
&& mouse_event.count == 1
&& cfg!(not(target_os = "macos"))
#[cfg(not(target_os = "macos"))]
if (data.tabs.len() > 1 && _mouse_event.count == 1)
|| data.config.lapce.custom_titlebar
{
for (rect, cmd) in self.tab_header_cmds.iter() {
if rect.contains(mouse_event.pos) {
if rect.contains(_mouse_event.pos) {
self.mouse_down_cmd = Some((*rect, cmd.clone()));
self.holding_click_rect = Some(*rect);
break;
}
}
@ -258,15 +269,29 @@ fn event(
.to(Target::Window(data.window_id)),
)
}
if data.tabs.len() > 1
&& mouse_event.count < 2
&& cfg!(not(target_os = "macos"))
#[cfg(not(target_os = "macos"))]
if (data.tabs.len() > 1 && mouse_event.count < 2)
|| data.config.lapce.custom_titlebar
{
if let Some((rect, cmd)) = self.mouse_down_cmd.as_ref() {
if rect.contains(mouse_event.pos) {
ctx.submit_command(cmd.clone());
}
}
for (rect, cmd) in self.tab_header_cmds.iter() {
if let Some(click_rect) = self.holding_click_rect {
if rect.contains(mouse_event.pos)
&& click_rect.contains(mouse_event.pos)
{
ctx.submit_command(cmd.clone());
ctx.set_handled();
}
}
}
self.holding_click_rect = None;
}
}
Event::Command(cmd) if cmd.is(LAPCE_UI_COMMAND) => {
@ -471,6 +496,12 @@ fn update(
if old_data.active != data.active {
ctx.request_layout();
}
#[cfg(not(platform_os = "macos"))]
if data.config.lapce.custom_titlebar != old_data.config.lapce.custom_titlebar
{
ctx.window()
.show_titlebar(!data.config.lapce.custom_titlebar);
}
let old_tab = old_data.tabs.get(&old_data.active_id).unwrap();
let tab = data.tabs.get(&data.active_id).unwrap();
if old_tab.workspace != tab.workspace {
@ -506,7 +537,9 @@ fn layout(
#[cfg(not(target_os = "macos"))]
let left_padding = 0.0;
#[cfg(target_os = "macos")]
let left_padding = if ctx.window().is_fullscreen() {
let left_padding = if ctx.window().is_fullscreen()
|| !data.config.lapce.custom_titlebar
{
0.0
} else {
78.0
@ -664,18 +697,38 @@ fn paint(&mut self, ctx: &mut PaintCtx, data: &LapceWindowData, env: &Env) {
);
self.tab_header_cmds.clear();
if cfg!(not(target_os = "macos")) {
let (cmds, svgs, text_layouts) = window_controls(
#[cfg(not(target_os = "macos"))]
if data.config.lapce.custom_titlebar {
let (cmds, svgs) = window_controls(
data.window_id,
&ctx.window().get_window_state(),
ctx.text(),
size.width - 36.0 * 3.0,
36.0,
&data.config,
);
self.tab_header_cmds = cmds;
for (svg, rect) in svgs {
for (svg, rect, color) in svgs {
let hover_rect = rect.inflate(10.0, 10.0);
if hover_rect.contains(self.mouse_pos)
&& (self.holding_click_rect.is_none()
|| self
.holding_click_rect
.unwrap()
.contains(self.mouse_pos))
{
ctx.fill(hover_rect, &color);
ctx.stroke(
Line::new(
Point::new(hover_rect.x0, hover_rect.y1),
Point::new(hover_rect.x1, hover_rect.y1),
),
data.config
.get_color_unchecked(LapceTheme::LAPCE_BORDER),
1.0,
);
}
ctx.draw_svg(
&svg,
rect,
@ -685,28 +738,26 @@ fn paint(&mut self, ctx: &mut PaintCtx, data: &LapceWindowData, env: &Env) {
),
);
}
for (text_layout, point) in text_layouts {
ctx.draw_text(&text_layout, point);
}
}
}
}
}
#[allow(clippy::type_complexity)]
#[cfg(not(target_os = "macos"))]
pub fn window_controls(
window_id: WindowId,
window_id: druid::WindowId,
window_state: &WindowState,
piet_text: &mut PietText,
x: f64,
width: f64,
config: &Config,
) -> (
Vec<(Rect, Command)>,
Vec<(Svg, Rect)>,
Vec<(PietTextLayout, Point)>,
Vec<(druid::piet::Svg, Rect, druid::Color)>,
) {
use crate::svg::get_svg;
use druid::Color;
let mut commands = Vec::new();
let minimise_rect = Size::new(width, width)
@ -748,72 +799,61 @@ pub fn window_controls(
Command::new(druid::commands::QUIT_APP, (), Target::Global),
));
let mut svgs = Vec::new();
if cfg!(target_os = "linux")
|| cfg!(target_os = "freebsd")
|| cfg!(target_os = "openbsd")
{
let minimize_rect = Size::new(width, width)
.to_rect()
.with_origin(Point::new(x, 0.0))
.inflate(-12.0, -12.0);
svgs.push((get_svg("chrome-minimize.svg").unwrap(), minimize_rect));
let max_res_rect = Size::new(width, width)
.to_rect()
.with_origin(Point::new(x + width, 0.0))
.inflate(-10.0, -10.0);
let max_res_svg = if window_state == &WindowState::Restored {
get_svg("chrome-maximize.svg").unwrap()
let hover_color = {
let (r, g, b, a) = config
.get_color_unchecked(LapceTheme::PANEL_BACKGROUND)
.to_owned()
.as_rgba8();
// TODO: hacky way to detect "lightness" of colour, should be fixed once we have dark/light themes
if r < 128 || g < 128 || b < 128 {
Color::rgba8(
r.saturating_add(25),
g.saturating_add(25),
b.saturating_add(25),
a,
)
} else {
get_svg("chrome-restore.svg").unwrap()
};
svgs.push((max_res_svg, max_res_rect));
let close_rect = Size::new(width, width)
.to_rect()
.with_origin(Point::new(x + 2.0 * width, 0.0))
.inflate(-10.0, -10.0);
svgs.push((get_svg("chrome-close.svg").unwrap(), close_rect));
}
let mut text_layouts = Vec::new();
if cfg!(target_os = "windows") {
let texts = vec![
"\u{E949}",
if window_state == &WindowState::Restored {
"\u{E739}"
} else {
"\u{E923}"
},
"\u{E106}",
];
let font_size = 10.0;
let font_family = "Segoe MDL2 Assets";
for (i, text_layout) in texts
.iter()
.map(|text| {
piet_text
.new_text_layout(text.to_string())
.font(piet_text.font_family(font_family).unwrap(), font_size)
.text_color(
config
.get_color_unchecked(LapceTheme::EDITOR_FOREGROUND)
.clone(),
)
.build()
.unwrap()
})
.enumerate()
{
let text_size = text_layout.size();
let point = Point::new(
x + i as f64 * width + ((text_size.width + 5.0) / 2.0),
(36.0 - text_size.height) / 2.0,
);
text_layouts.push((text_layout, point));
Color::rgba8(
r.saturating_sub(30),
g.saturating_sub(30),
b.saturating_sub(30),
a,
)
}
}
};
(commands, svgs, text_layouts)
let mut svgs = Vec::new();
let minimize_rect = Size::new(width, width)
.to_rect()
.with_origin(Point::new(x, 0.0))
.inflate(-10.0, -10.0);
svgs.push((
get_svg("chrome-minimize.svg").unwrap(),
minimize_rect,
hover_color.clone(),
));
let max_res_rect = Size::new(width, width)
.to_rect()
.with_origin(Point::new(x + width, 0.0))
.inflate(-10.0, -10.0);
let max_res_svg = if window_state == &WindowState::Restored {
get_svg("chrome-maximize.svg").unwrap()
} else {
get_svg("chrome-restore.svg").unwrap()
};
svgs.push((max_res_svg, max_res_rect, hover_color));
let close_rect = Size::new(width, width)
.to_rect()
.with_origin(Point::new(x + 2.0 * width, 0.0))
.inflate(-10.0, -10.0);
svgs.push((
get_svg("chrome-close.svg").unwrap(),
close_rect,
Color::rgb8(210, 16, 33),
));
(commands, svgs)
}