diff --git a/.gitignore b/.gitignore index fa8f71b51ae3c0c2420f264609d9a800a577fea0..67c248ee9d0fb15e118ace7cf53072910116a9da 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,6 @@ debian/*.debhelper.log debian/gxde-terminal/ /gxde-terminal *.gmo + +# IDE +.cache/* diff --git a/debian/control b/debian/control index 6fc82cae4d4bee7699a0695b57a450c07ef31793..fb14c1e061a6666200b1e61771ae833eb3747f1e 100644 --- a/debian/control +++ b/debian/control @@ -35,7 +35,6 @@ Architecture: any Depends: ${shlibs:Depends}, ${misc:Depends}, - deepin-menu | dde-launchpad, expect, lrzsz, zssh diff --git a/image/menu/dark/check_disabled.png b/image/menu/dark/check_disabled.png new file mode 100644 index 0000000000000000000000000000000000000000..574f9fbbc8fe54201d44ca2c0f4112ca9c2b14f5 Binary files /dev/null and b/image/menu/dark/check_disabled.png differ diff --git a/image/menu/dark/check_normal.png b/image/menu/dark/check_normal.png new file mode 100644 index 0000000000000000000000000000000000000000..5025dee090d5d6b10fabf5d9c299ca2e79de2607 Binary files /dev/null and b/image/menu/dark/check_normal.png differ diff --git a/image/menu/dark/check_selected.png b/image/menu/dark/check_selected.png new file mode 100644 index 0000000000000000000000000000000000000000..42945bdf3ec8856c693e40cb288ae82c76b74e3f Binary files /dev/null and b/image/menu/dark/check_selected.png differ diff --git a/image/menu/light/arrow-right_disabled.svg b/image/menu/light/arrow-right_disabled.svg new file mode 100644 index 0000000000000000000000000000000000000000..ce9ec2fefea6556f3c11349110354e4ee2cf253f --- /dev/null +++ b/image/menu/light/arrow-right_disabled.svg @@ -0,0 +1,12 @@ + + + + 菜单箭头 + Created with Sketch. + + + + + + + \ No newline at end of file diff --git a/image/menu/light/arrow-right_normal.svg b/image/menu/light/arrow-right_normal.svg new file mode 100644 index 0000000000000000000000000000000000000000..ce9ec2fefea6556f3c11349110354e4ee2cf253f --- /dev/null +++ b/image/menu/light/arrow-right_normal.svg @@ -0,0 +1,12 @@ + + + + 菜单箭头 + Created with Sketch. + + + + + + + \ No newline at end of file diff --git a/image/menu/light/arrow-right_selected.svg b/image/menu/light/arrow-right_selected.svg new file mode 100644 index 0000000000000000000000000000000000000000..20e48cf149cdc93d2122ce0cb3905567c17f0463 --- /dev/null +++ b/image/menu/light/arrow-right_selected.svg @@ -0,0 +1,12 @@ + + + + 菜单箭头选中 + Created with Sketch. + + + + + + + \ No newline at end of file diff --git a/image/menu/light/check_disabled.svg b/image/menu/light/check_disabled.svg new file mode 100644 index 0000000000000000000000000000000000000000..754da569d33826807b1fd2d7c6e948e872ba2006 --- /dev/null +++ b/image/menu/light/check_disabled.svg @@ -0,0 +1,14 @@ + + + + select_insensitive + Created with Sketch. + + + + + + + + + \ No newline at end of file diff --git a/image/menu/light/check_normal.svg b/image/menu/light/check_normal.svg new file mode 100644 index 0000000000000000000000000000000000000000..854a7d2310503bd7b701e99a0d730b3f98c6066c --- /dev/null +++ b/image/menu/light/check_normal.svg @@ -0,0 +1,14 @@ + + + + select + Created with Sketch. + + + + + + + + + \ No newline at end of file diff --git a/image/menu/light/check_selected.svg b/image/menu/light/check_selected.svg new file mode 100644 index 0000000000000000000000000000000000000000..0974bab2cec148bd95bd4302fd5517c6b857acc6 --- /dev/null +++ b/image/menu/light/check_selected.svg @@ -0,0 +1,14 @@ + + + + select_hover + Created with Sketch. + + + + + + + + + \ No newline at end of file diff --git a/lib/config.vala b/lib/config.vala index 5ed1c425b4177008c2660b00b7015dac41879290..72f5a0f66048da10a910b17661ff93bde15cd421 100644 --- a/lib/config.vala +++ b/lib/config.vala @@ -142,7 +142,6 @@ namespace Config { config_file.set_boolean("advanced", "audible_bell", false); config_file.set_boolean("advanced", "always_hide_resize_grip", false); config_file.set_boolean("advanced", "allow_hyperlink", false); - config_file.set_boolean("advanced", "prefer_deepin_menu", true); config_file.set_string("advanced", "background_image", ""); @@ -343,7 +342,6 @@ namespace Config { check_boolean("advanced", "audible_bell", false); check_boolean("advanced", "always_hide_resize_grip", false); check_boolean("advanced", "allow_hyperlink", false); - check_boolean("advanced", "prefer_deepin_menu", true); check_boolean("advanced", "scroll_on_key", true); check_boolean("advanced", "scroll_on_output", false); diff --git a/lib/menu.vala b/lib/menu.vala index 4ad851836fc0f9f3b917e5a26d4006d9e27f3fd4..49ff7e0519d7f1b3f6fba1817df816f03b6af165 100644 --- a/lib/menu.vala +++ b/lib/menu.vala @@ -24,18 +24,10 @@ using Gtk; namespace Menu { - [DBus (name = "com.deepin.menu.Manager")] - interface MenuManagerInterface : Object { - public abstract string RegisterMenu() throws Error; - public abstract void UnregisterMenu(string object_path) throws Error; - } - - [DBus (name = "com.deepin.menu.Menu")] - interface MenuInterface : Object { - public abstract void ShowMenu(string menu_json_content) throws Error; - public signal void ItemInvoked(string item_id, bool checked); - public signal void MenuUnregistered(); - } + [CCode (cheader_filename = "wayland_blur.h", cname = "gxde_set_blur_region")] + private extern void wayland_set_blur_region (Gdk.Window window, int x, int y, int width, int height, int radius); + [CCode (cheader_filename = "wayland_blur.h", cname = "gxde_blur_available")] + private extern bool wayland_blur_available (Gdk.Display display); public class MenuItem : Object { public string menu_item_id; @@ -60,244 +52,362 @@ namespace Menu { } } + // 用 GTK 菜单复刻 deepin-menu 的外观,尺寸与颜色取自 dstyle 插件 dlight2/ddark2 与 DTK 的 DMenuEffect。 public class Menu : Object { - MenuManagerInterface menu_manager_interface; - MenuInterface menu_interface; - static string? menu_object_path; - - private bool prefer_deepin_menu = true; - private bool inited = false; + private const int RADIUS = 8; + private const int ITEM_HEIGHT = 22; + private const int CHECK_X = 10; + private const int TEXT_X = 27; + private const int TEXT_RIGHT_PADDING = 44; + private const int ARROW_RIGHT_MARGIN = 6; + private const double BLUR_BACKGROUND_ALPHA = 0.45; + + private static Gtk.CssProvider? css_provider = null; + private static HashTable icon_cache = null; + + private Gtk.Menu? gtk_menu = null; + private bool is_dark = false; + private bool blur_enabled = false; + private int scale_factor = 1; public signal void click_item(string item_id); public signal void destroy(); public Menu() {} - public void set_prefer_deepin_menu(bool _prefer_deepin_menu) { - prefer_deepin_menu = _prefer_deepin_menu; - } + // 在指针处弹出(右键菜单、键盘菜单键)。 + public void popup_at_pointer(List menu_content, Gtk.Widget attach_widget, Gdk.Event? trigger_event) { + prepare(menu_content, attach_widget); - private void init() { - if (inited) return; + var toplevel_window = attach_widget.get_toplevel().get_window(); + Gdk.Device? pointer = trigger_event != null ? trigger_event.get_device() : null; + if (pointer == null) { + pointer = attach_widget.get_display().get_default_seat().get_pointer(); + } + int x, y; + toplevel_window.get_device_position(pointer, out x, out y, null); + popup_at_toplevel_rect(toplevel_window, { x, y, 1, 1 }, Gdk.Gravity.NORTH_WEST, Gdk.Gravity.NORTH_WEST, trigger_event); + } - register_deepin_menu_dbus_if_needed(); + // 贴着控件弹出,例如标题栏菜单按钮。 + public void popup_at_widget(List menu_content, Gtk.Widget widget, Gdk.Gravity widget_anchor, Gdk.Gravity menu_anchor, Gdk.Event? trigger_event) { + prepare(menu_content, widget); - inited = true; + var toplevel = widget.get_toplevel(); + int x, y; + widget.translate_coordinates(toplevel, 0, 0, out x, out y); + popup_at_toplevel_rect(toplevel.get_window(), { x, y, widget.get_allocated_width(), widget.get_allocated_height() }, widget_anchor, menu_anchor, trigger_event); } - private void register_deepin_menu_dbus_if_needed() { - if (prefer_deepin_menu) { - try { - menu_manager_interface = Bus.get_proxy_sync(BusType.SESSION, "com.deepin.menu", "/com/deepin/menu"); - - if (menu_object_path != null) - unregister(); - menu_object_path = menu_manager_interface.RegisterMenu(); - - menu_interface = Bus.get_proxy_sync(BusType.SESSION, "com.deepin.menu", menu_object_path); - menu_interface.ItemInvoked.connect((item_id, checked) => { - click_item(item_id); - }); - menu_interface.MenuUnregistered.connect(() => { - menu_object_path = null; - destroy(); - }); - } catch (Error e) { - prefer_deepin_menu = false; - stderr.printf ("%s\n", e.message); - } - } + // 在相对 widget 的 (x, y) 处弹出,用于键盘触发等没有指针事件的场景。 + public void popup_at_rect(List menu_content, Gtk.Widget widget, int x, int y, Gdk.Event? trigger_event) { + prepare(menu_content, widget); + + var toplevel = widget.get_toplevel(); + int window_x, window_y; + widget.translate_coordinates(toplevel, x, y, out window_x, out window_y); + popup_at_toplevel_rect(toplevel.get_window(), { window_x, window_y, 1, 1 }, Gdk.Gravity.NORTH_WEST, Gdk.Gravity.NORTH_WEST, trigger_event); } - private void unregister(){ - try { - menu_manager_interface.UnregisterMenu(menu_object_path); - } catch (Error e) { - stderr.printf ("%s\n", e.message); - } - menu_object_path = null; + // 统一以顶层窗口为锚点:EventBox、VTE 等控件有自己的子 GdkWindow, + // Wayland 下 GDK 不会把子窗口坐标换算到顶层 surface,直接用 GTK 的 popup_at_widget/pointer 会错位。 + private void popup_at_toplevel_rect(Gdk.Window toplevel_window, Gdk.Rectangle rect, Gdk.Gravity rect_anchor, Gdk.Gravity menu_anchor, Gdk.Event? trigger_event) { + gtk_menu.popup_at_rect(toplevel_window, rect, rect_anchor, menu_anchor, trigger_event); } - public void popup_at_position(List menu_content, int x, int y) { - init(); + private void prepare(List menu_content, Gtk.Widget attach_widget) { + is_dark = detect_dark_style(); + blur_enabled = wayland_blur_available(attach_widget.get_display()); + scale_factor = attach_widget.get_scale_factor(); + load_css(attach_widget.get_screen()); + + gtk_menu = create_menu(menu_content); + gtk_menu.attach_to_widget(attach_widget, null); + gtk_menu.deactivate.connect(() => { + // deactivate 先于菜单项的 activate 发出,延后收尾以保证 click_item 先送达。 + GLib.Idle.add(() => { + finish(); + return false; + }); + }); + gtk_menu.show_all(); + } - if (prefer_deepin_menu) { - show_deepin_menu(menu_content, x, y); - } else { - show_gtk_menu_at_pointer(menu_content); + private void finish() { + if (gtk_menu == null) { + return; } + + gtk_menu.destroy(); + gtk_menu = null; + destroy(); } - private Gtk.Menu create_gtk_menu(List menu_content) { - Gdk.Screen screen = Gdk.Screen.get_default(); - CssProvider provider = new Gtk.CssProvider(); - try { - provider.load_from_data(Utils.get_menu_css()); - } catch (GLib.Error e) { - warning("Something bad happened with CSS load %s", e.message); - } - Gtk.StyleContext.add_provider_for_screen(screen,provider,Gtk.STYLE_PROVIDER_PRIORITY_USER); - Gtk.Menu result = new Gtk.Menu(); - - foreach (unowned MenuItem menu_item in menu_content) { - var item = create_gtk_menu_item(menu_item); - if (menu_item.menu_item_submenu.length() > 0) { - Gtk.Menu submenu = create_gtk_menu(menu_item.menu_item_submenu); - item.set_submenu(submenu); - } - result.append(item); + private bool detect_dark_style() { + // deepin-menu 跟随 QT_STYLE_OVERRIDE(dlight2/ddark2),没有时退回 GTK 主题名判断。 + string? qt_style = Environment.get_variable("QT_STYLE_OVERRIDE"); + if (qt_style != null && qt_style != "") { + return qt_style.down().contains("dark"); } - return result; + string theme_name = Gtk.Settings.get_default().gtk_theme_name ?? ""; + return theme_name.down().contains("dark"); } - private Gtk.MenuItem create_gtk_menu_item(MenuItem menu_item) { - Gtk.MenuItem item; + private Gtk.Menu create_menu(List menu_content) { + var menu = new Gtk.Menu(); + // 勾选标记由菜单项自己绘制,不需要 GTK 预留 toggle 列。 + menu.reserve_toggle_size = false; + menu.get_style_context().add_class("gxde-menu"); + menu.get_style_context().add_class(is_dark ? "dark" : "light"); + if (blur_enabled) { + menu.get_style_context().add_class("blur"); + } - if (menu_item.menu_item_text == "") { - item = new Gtk.SeparatorMenuItem(); - } else if (menu_item.menu_item_checkable) { - item = new Gtk.CheckMenuItem.with_label(menu_item.menu_item_text); - ((Gtk.CheckMenuItem) item).active = menu_item.menu_item_checked; + var toplevel = menu.get_toplevel(); + toplevel.get_style_context().add_class("gxde-menu-window"); + + var screen = toplevel.get_screen(); + if (screen.is_composited()) { + toplevel.set_visual(screen.get_rgba_visual()); } else { - item = new Gtk.MenuItem.with_label(menu_item.menu_item_text); + menu.get_style_context().add_class("square"); } - item.activate.connect(() => { - click_item(menu_item.menu_item_id); - }); - - item.sensitive = menu_item.menu_item_sensitive; + menu.size_allocate.connect_after((w, a) => { + update_surface(menu); + }); + toplevel.map.connect_after((w) => { + update_surface(menu); + }); - return item; - } + foreach (unowned MenuItem menu_item in menu_content) { + menu.append(create_menu_item(menu_item)); + } - private void show_gtk_menu_at_pointer(List menu_content) { - var gtk_menu = create_gtk_menu(menu_content); - gtk_menu.show_all(); - gtk_menu.popup_at_pointer(); + return menu; } - private void show_deepin_menu(List menu_content, int x, int y) { - // since GTK only supports integral scaling yet DDE supports fractional scaling, - // the scale on both sides may not be the same, so we need to negtiate here. - var scale = Utils.get_default_monitor_scale(); - var dde_scale = Utils.get_dde_scale_ratio(); - - if (scale != dde_scale) { - x = (int)(x * scale / dde_scale); - y = (int)(y * scale / dde_scale); + private void update_surface(Gtk.Menu menu) { + var toplevel = menu.get_toplevel(); + var window = toplevel.get_window(); + if (window == null) { + return; } - try { - Json.Builder builder = new Json.Builder(); + int x, y; + menu.translate_coordinates(toplevel, 0, 0, out x, out y); + int width = menu.get_allocated_width(); + int height = menu.get_allocated_height(); - builder.begin_object(); + // 把阴影声明给合成器,让定位和贴边避让只按可见面板计算, + // 否则靠近屏幕边缘时菜单会多让出一个阴影宽度,与按钮对不齐。 + window.set_shadow_width(x, toplevel.get_allocated_width() - x - width, + y, toplevel.get_allocated_height() - y - height); - builder.set_member_name("x"); - builder.add_int_value(x); + if (blur_enabled) { + wayland_set_blur_region(window, x, y, width, height, RADIUS); + } + } - builder.set_member_name("y"); - builder.add_int_value(y); + private Gtk.MenuItem create_menu_item(MenuItem menu_item) { + if (menu_item.menu_item_text == "") { + return new Gtk.SeparatorMenuItem(); + } - builder.set_member_name("isDockMenu"); - builder.add_boolean_value(false); + var item = new Gtk.MenuItem(); + var box = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 0); - builder.set_member_name("menuJsonContent"); - builder.add_string_value(get_items_node(menu_content)); + // 勾选标记位于 x=10,文字从 x=27 开始,与 dstyle 的 drawMenuItemControl 一致。 + var check_image = new Gtk.Image(); + check_image.set_size_request(TEXT_X - CHECK_X, -1); + check_image.halign = Gtk.Align.START; + check_image.xalign = 0; + check_image.margin_start = CHECK_X; + box.pack_start(check_image, false, false, 0); - builder.set_member_name("isScaled"); - builder.add_boolean_value(false); + var label = new Gtk.Label(menu_item.menu_item_text); + label.halign = Gtk.Align.START; + label.margin_end = TEXT_RIGHT_PADDING; + box.pack_start(label, true, true, 0); - builder.end_object (); + var arrow_image = new Gtk.Image(); + arrow_image.margin_end = ARROW_RIGHT_MARGIN; + box.pack_end(arrow_image, false, false, 0); - Json.Generator generator = new Json.Generator(); - Json.Node root = builder.get_root(); - generator.set_root(root); + item.add(box); + item.set_size_request(-1, ITEM_HEIGHT); + item.sensitive = menu_item.menu_item_sensitive; - string menu_json_content = generator.to_data(null); + bool has_submenu = menu_item.menu_item_submenu.length() > 0; + bool show_check = menu_item.menu_item_checkable && menu_item.menu_item_checked; - menu_interface.ShowMenu(menu_json_content); - } catch (Error e) { - stderr.printf ("%s\n", e.message); + if (has_submenu) { + item.set_submenu(create_menu(menu_item.menu_item_submenu)); + } else { + string item_id = menu_item.menu_item_id; + item.activate.connect(() => { + click_item(item_id); + }); } - } - private string get_items_node(List menu_content) { - Json.Builder builder = new Json.Builder(); + // 根据高亮/禁用状态切换图标,对应 dstyle 的 _normal/_selected/_disabled。 + item.state_flags_changed.connect((w, previous) => { + update_item_icons(item, check_image, arrow_image, show_check, has_submenu); + }); + update_item_icons(item, check_image, arrow_image, show_check, has_submenu); - builder.begin_object(); + return item; + } - builder.set_member_name("items"); - builder.begin_array (); - foreach (MenuItem item in menu_content) { - builder.add_value(get_item_node(item)); + private void update_item_icons(Gtk.Widget item, Gtk.Image check_image, Gtk.Image arrow_image, bool show_check, bool has_submenu) { + var flags = item.get_state_flags(); + string state = "normal"; + if (Gtk.StateFlags.INSENSITIVE in flags) { + state = "disabled"; + } else if (Gtk.StateFlags.PRELIGHT in flags) { + state = "selected"; } - builder.end_array (); - - builder.end_object (); - - Json.Generator generator = new Json.Generator(); - generator.set_root(builder.get_root()); - return generator.to_data(null); + check_image.set_from_surface(show_check ? load_icon("check", state, scale_factor) : null); + arrow_image.set_from_surface(has_submenu ? load_icon("arrow-right", state, scale_factor) : null); } - private Json.Node get_item_node(MenuItem item) { - Json.Builder builder = new Json.Builder(); - - builder.begin_object(); - - builder.set_member_name("itemId"); - builder.add_string_value(item.menu_item_id); - - builder.set_member_name("itemText"); - builder.add_string_value(item.menu_item_text); - - builder.set_member_name("itemIcon"); - builder.add_string_value(""); - - builder.set_member_name("itemIconHover"); - builder.add_string_value(""); - - builder.set_member_name("itemIconInactive"); - builder.add_string_value(""); - - builder.set_member_name("itemExtra"); - builder.add_string_value(""); + private Cairo.Surface? load_icon(string name, string state, int scale) { + if (icon_cache == null) { + icon_cache = new HashTable(str_hash, str_equal); + } - builder.set_member_name("isActive"); - builder.add_boolean_value(item.menu_item_sensitive); + string style_name = is_dark ? "dark" : "light"; + string key = "%s/%s_%s@%d".printf(style_name, name, state, scale); + if (icon_cache.contains(key)) { + return icon_cache.get(key); + } - builder.set_member_name("checked"); - builder.add_boolean_value(item.menu_item_checked); + Cairo.Surface? surface = null; + // 与 dstyle 相同的查找顺序:先找对应状态,找不到退回 _normal。 + foreach (string icon_state in new string[] { state, "normal" }) { + foreach (string format in new string[] { "svg", "png" }) { + string path = Utils.get_image_path("menu/%s/%s_%s.%s".printf(style_name, name, icon_state, format)); + if (!FileUtils.test(path, FileTest.EXISTS)) { + continue; + } + + try { + int width, height; + Gdk.Pixbuf.get_file_info(path, out width, out height); + var pixbuf = new Gdk.Pixbuf.from_file_at_scale(path, width * scale, height * scale, true); + surface = Gdk.cairo_surface_create_from_pixbuf(pixbuf, scale, null); + } catch (GLib.Error e) { + warning("Load menu icon %s failed: %s", path, e.message); + } + + if (surface != null) { + break; + } + } - builder.set_member_name("itemSubMenu"); - unowned List submenu_items = item.menu_item_submenu; + if (surface != null) { + break; + } + } - if (submenu_items.length() == 0) { - builder.add_null_value(); - } else { - Json.Builder _builder = new Json.Builder(); + icon_cache.insert(key, surface); + return surface; + } - _builder.begin_object (); + private void load_css(Gdk.Screen screen) { + if (css_provider == null) { + css_provider = new Gtk.CssProvider(); + Gtk.StyleContext.add_provider_for_screen(screen, css_provider, Gtk.STYLE_PROVIDER_PRIORITY_USER); + } - _builder.set_member_name("items"); + try { + css_provider.load_from_data(get_menu_css()); + } catch (GLib.Error e) { + warning("Something bad happened with CSS load %s", e.message); + } + } - _builder.begin_array (); - foreach (MenuItem _item in submenu_items) { - _builder.add_value(get_item_node(_item)); + // 取 DTK 的 Qt 字体设置,使菜单文字与 deepin-menu 一致。 + private string get_font_css() { + var key_file = new KeyFile(); + try { + key_file.load_from_file(Path.build_filename(Environment.get_user_config_dir(), "deepin", "qt-theme.ini"), KeyFileFlags.NONE); + string font_css = ""; + if (key_file.has_key("Theme", "Font")) { + font_css += "font-family: \"%s\";".printf(key_file.get_string("Theme", "Font")); } - _builder.end_array (); - - _builder.end_object (); - - builder.add_value(_builder.get_root()); + if (key_file.has_key("Theme", "FontSize")) { + font_css += "font-size: %spt;".printf(key_file.get_string("Theme", "FontSize")); + } + return font_css; + } catch (GLib.Error e) { + return ""; } + } - builder.end_object (); + private string get_menu_css() { + string font_css = get_font_css(); - return builder.get_root(); + return @" + window.gxde-menu-window, + window.gxde-menu-window.background { + background-color: transparent; + } + window.gxde-menu-window decoration, + window.gxde-menu-window decoration:backdrop { + margin: 0; + border-radius: $(RADIUS)px; + box-shadow: 0 3px 14px rgba(0, 0, 0, 0.18), 0 0 0 1px rgba(0, 0, 0, 0.08); + } + menu.gxde-menu { + border: none; + border-radius: $(RADIUS)px; + margin: 0; + /* 上下留白不小于圆角半径,首末项高亮才不会顶进圆角里 */ + padding: $(RADIUS)px 0; + } + menu.gxde-menu.square { border-radius: 0; } + menu.gxde-menu.light { background-color: rgb(255, 255, 255); } + menu.gxde-menu.light.blur { background-color: rgba(255, 255, 255, $(BLUR_BACKGROUND_ALPHA)); } + menu.gxde-menu.dark { background-color: #202020; } + menu.gxde-menu.dark.blur { background-color: rgba(32, 32, 32, $(BLUR_BACKGROUND_ALPHA)); } + menu.gxde-menu menuitem { + min-height: $(ITEM_HEIGHT)px; + padding: 0; + margin: 0; + border: none; + border-radius: 0; + background: none; + box-shadow: none; + $font_css + } + menu.gxde-menu menuitem label { + color: inherit; + $font_css + } + menu.gxde-menu menuitem arrow { + -gtk-icon-source: none; + min-width: 0; + min-height: 0; + margin: 0; + padding: 0; + } + menu.gxde-menu separator { + min-height: 1px; + margin: 2px 5px; + padding: 0; + } + menu.gxde-menu.light menuitem { color: rgb(25, 25, 25); } + menu.gxde-menu.light menuitem:disabled { color: rgba(0, 0, 0, 0.4); } + menu.gxde-menu.light menuitem:hover { background-color: #2ca7f8; color: #ffffff; } + menu.gxde-menu.light separator { background-color: rgba(0, 0, 0, 0.1); } + menu.gxde-menu.dark menuitem { color: #ffffff; } + menu.gxde-menu.dark menuitem:disabled { color: rgba(255, 255, 255, 0.4); } + menu.gxde-menu.dark menuitem:hover { background-color: #61b5f8; color: #ffffff; } + menu.gxde-menu.dark separator { background-color: rgba(255, 255, 255, 0.1); } + "; } } } diff --git a/lib/utils.vala b/lib/utils.vala index 0b549870404055421e17cc74e4ea65ff3e510861..f81f87975fe966337aaac3f65cd22fb9d3f6f3bf 100644 --- a/lib/utils.vala +++ b/lib/utils.vala @@ -129,40 +129,6 @@ namespace Utils { is_tiling = true; } - public string get_menu_css() { - string background_color = "#ffffff"; - string foreground_color = "#000000"; - bool is_light_theme = true; - try { - Config.Config config = new Config.Config(); - background_color = config.config_file.get_string("theme", "background"); - foreground_color = config.config_file.get_string("theme", "foreground"); - is_light_theme = config.config_file.get_string("theme", "style") == "light"; - } catch (GLib.KeyFileError e) { - print("Can't read config file: %s\n", e.message); - } - string text_color = is_light_theme ? "white" : "black"; - string text_color_on_hover = is_light_theme ? "black" : "white"; - - return @" - menu { - background: $background_color; - opacity: 0.95; - border: 0; - } - menu menuitem{ - color: $text_color_on_hover; - background: $background_color; - opacity: 0.95; - } - menu menuitem:hover { - background-color: $foreground_color; - color: $text_color; - transition: color 10ms linear; - } - "; - } - public void touch_dir(string dir) { var dir_file = GLib.File.new_for_path(dir); if (!dir_file.query_exists()) { @@ -590,20 +556,6 @@ namespace Utils { return 1.0; } - public double get_dde_scale_ratio() { - var scaleStr = GLib.Environment.get_variable("QT_SCALE_FACTOR"); - if (scaleStr == null) { - var dpiStr = GLib.Environment.get_variable("QT_FONT_DPI"); - if (dpiStr == null) { - return 1.0; - } - return double.parse(dpiStr)/96; - } - - var parsedScale = double.parse(scaleStr); - return parsedScale == 0 ? 1.0 : parsedScale; - } - public int get_pointer_monitor(Gdk.Screen screen) { int x, y; get_pointer_position(out x, out y); diff --git a/lib/wayland_blur.c b/lib/wayland_blur.c index c04a368b71338dcfd1a9090da04f82e0cf1abd62..5072a0b4e87699482907ee5b5c5daa77bb9aba24 100644 --- a/lib/wayland_blur.c +++ b/lib/wayland_blur.c @@ -1,3 +1,4 @@ +#include #include #include "wayland_blur.h" @@ -125,6 +126,33 @@ static struct org_kde_kwin_blur* get_or_create_kde_blur(GdkWindow* window, return blur; } +/* wl_region 只支持矩形,圆角部分按行拆成1像素高的横条 + * 原来还要自己拼.png + */ +static void add_rounded_rect(struct wl_region* region, int x, int y, + int width, int height, int radius) { + if (radius > width / 2) { + radius = width / 2; + } + + if (radius > height / 2) { + radius = height / 2; + } + + if (radius <= 0) { + wl_region_add(region, x, y, width, height); + return; + } + + for (int i = 0; i < radius; i++) { + double dy = radius - i - 0.5; + int inset = (int) lround(radius - sqrt((double) radius * radius - dy * dy)); + wl_region_add(region, x + inset, y + i, width - inset * 2, 1); + wl_region_add(region, x + inset, y + height - 1 - i, width - inset * 2, 1); + } + wl_region_add(region, x, y + radius, width, height - radius * 2); +} + static int resolve_window(GdkWindow* window, struct wl_display** display_out, struct wl_surface** surface_out) { if (window == NULL || !GDK_IS_WAYLAND_WINDOW(window)) { @@ -148,8 +176,8 @@ static int resolve_window(GdkWindow* window, struct wl_display** display_out, return 1; } -void gxde_set_blur_region(GdkWindow* window, int x, int y, int width, - int height) { +static void wl_set_blur_region(GdkWindow* window, int x, int y, int width, + int height, int radius) { struct wl_display* display; struct wl_surface* surface; if (!resolve_window(window, &display, &surface)) { @@ -158,7 +186,28 @@ void gxde_set_blur_region(GdkWindow* window, int x, int y, int width, ensure_globals(display); - /* 优先 Treeland:整窗背景模糊,不需要区域。 */ + /* 优先 KDE:按区域模糊,可以把圆角外的部分排除掉。 */ + if (g_kde_blur_manager != NULL && g_compositor != NULL) { + struct org_kde_kwin_blur* blur = get_or_create_kde_blur(window, surface); + if (blur == NULL) { + return; + } + + struct wl_region* region = wl_compositor_create_region(g_compositor); + if (region == NULL) { + return; + } + add_rounded_rect(region, x, y, width, height, radius); + + org_kde_kwin_blur_set_region(blur, region); + org_kde_kwin_blur_commit(blur); + wl_region_destroy(region); + + wl_display_flush(display); + return; + } + + /* 回退 Treeland:整窗背景模糊,不支持区域。 */ if (g_treeland_manager != NULL) { struct treeland_personalization_window_context_v1* ctx = get_or_create_treeland_ctx(window, surface); @@ -166,35 +215,14 @@ void gxde_set_blur_region(GdkWindow* window, int x, int y, int width, treeland_personalization_window_context_v1_set_blend_mode( ctx, TREELAND_PERSONALIZATION_WINDOW_CONTEXT_V1_BLEND_MODE_BLUR); + treeland_personalization_window_context_v1_set_round_corner_radius( + ctx, radius); wl_display_flush(display); } - return; - } - - /* 回退KDE: 按内容区域模糊。 */ - if (g_kde_blur_manager == NULL || g_compositor == NULL) { - return; } - - struct org_kde_kwin_blur* blur = get_or_create_kde_blur(window, surface); - if (blur == NULL) { - return; - } - - struct wl_region* region = wl_compositor_create_region(g_compositor); - if (region == NULL) { - return; - } - wl_region_add(region, x, y, width, height); - - org_kde_kwin_blur_set_region(blur, region); - org_kde_kwin_blur_commit(blur); - wl_region_destroy(region); - - wl_display_flush(display); } -void gxde_clear_blur(GdkWindow* window) { +static void wl_clear_blur(GdkWindow* window) { struct wl_display* display; struct wl_surface* surface; if (!resolve_window(window, &display, &surface)) { @@ -203,6 +231,14 @@ void gxde_clear_blur(GdkWindow* window) { ensure_globals(display); + /* KDE:移除缓存的 blur 对象并 unset。 */ + if (g_kde_blur_manager != NULL && g_compositor != NULL) { + g_object_set_data(G_OBJECT(window), GXDE_KDE_BLUR_KEY, NULL); + org_kde_kwin_blur_manager_unset(g_kde_blur_manager, surface); + wl_display_flush(display); + return; + } + /* Treeland: blend mode -> 透明。 */ if (g_treeland_manager != NULL) { struct treeland_personalization_window_context_v1* ctx = @@ -213,31 +249,163 @@ void gxde_clear_blur(GdkWindow* window) { TREELAND_PERSONALIZATION_WINDOW_CONTEXT_V1_BLEND_MODE_TRANSPARENT); wl_display_flush(display); } - return; } +} - /* KDE:移除缓存的 blur 对象并 unset。 */ - g_object_set_data(G_OBJECT(window), GXDE_KDE_BLUR_KEY, NULL); - if (g_kde_blur_manager != NULL) { - org_kde_kwin_blur_manager_unset(g_kde_blur_manager, surface); - wl_display_flush(display); +static int wl_blur_available(GdkDisplay* gdk_display) { + if (gdk_display == NULL || !GDK_IS_WAYLAND_DISPLAY(gdk_display)) { + return 0; + } + + struct wl_display* display = gdk_wayland_display_get_wl_display( + gdk_display); + if (display == NULL) { + return 0; + } + + ensure_globals(display); + + return (g_kde_blur_manager != NULL && g_compositor != NULL) + || g_treeland_manager != NULL; +} + +#endif /* GDK_WINDOWING_WAYLAND */ + +#ifdef GDK_WINDOWING_X11 +#include +#include +#include + +static int x11_list_contains_atom(Display* xdisplay, Window window, + Atom list_atom, Atom target) { + Atom type; + int format; + unsigned long count, remaining; + unsigned char* data = NULL; + int found = 0; + + if (XGetWindowProperty(xdisplay, window, list_atom, 0, 4096, False, + XA_ATOM, &type, &format, &count, &remaining, &data) == Success + && data != NULL) { + Atom* atoms = (Atom*) data; + for (unsigned long i = 0; i < count; i++) { + if (atoms[i] == target) { + found = 1; + break; + } + } + } + + if (data != NULL) { + XFree(data); + } + return found; +} + +static int x11_blur_available(GdkDisplay* gdk_display) { + if (!gdk_screen_is_composited(gdk_display_get_default_screen(gdk_display))) { + return 0; + } + + Display* xdisplay = GDK_DISPLAY_XDISPLAY(gdk_display); + Window root = DefaultRootWindow(xdisplay); + + Atom net_supported = XInternAtom(xdisplay, "_NET_SUPPORTED", False); + Atom deepin_blur = XInternAtom(xdisplay, "_NET_WM_DEEPIN_BLUR_REGION_ROUNDED", False); + if (x11_list_contains_atom(xdisplay, root, net_supported, deepin_blur)) { + return 1; + } + + Atom kde_blur = XInternAtom(xdisplay, "_KDE_NET_WM_BLUR_BEHIND_REGION", False); + int count = 0; + Atom* properties = XListProperties(xdisplay, root, &count); + int found = 0; + for (int i = 0; i < count; i++) { + if (properties[i] == kde_blur) { + found = 1; + break; + } + } + if (properties != NULL) { + XFree(properties); } + return found; } -#else /* !GDK_WINDOWING_WAYLAND */ +static void x11_set_blur_region(GdkWindow* window, int x, int y, int width, + int height, int radius) { + Display* xdisplay = GDK_WINDOW_XDISPLAY(window); + Window xid = GDK_WINDOW_XID(window); + int scale = gdk_window_get_scale_factor(window); + + long deepin_data[6] = { x * scale, y * scale, width * scale, height * scale, + radius * scale, radius * scale }; + long kde_data[4] = { x * scale, y * scale, width * scale, height * scale }; + + XChangeProperty(xdisplay, xid, + XInternAtom(xdisplay, "_NET_WM_DEEPIN_BLUR_REGION_ROUNDED", False), + XA_CARDINAL, 32, PropModeReplace, (unsigned char*) deepin_data, 6); + XChangeProperty(xdisplay, xid, + XInternAtom(xdisplay, "_KDE_NET_WM_BLUR_BEHIND_REGION", False), + XA_CARDINAL, 32, PropModeReplace, (unsigned char*) kde_data, 4); + XFlush(xdisplay); +} + +static void x11_clear_blur(GdkWindow* window) { + Display* xdisplay = GDK_WINDOW_XDISPLAY(window); + Window xid = GDK_WINDOW_XID(window); + + XDeleteProperty(xdisplay, xid, + XInternAtom(xdisplay, "_NET_WM_DEEPIN_BLUR_REGION_ROUNDED", False)); + XDeleteProperty(xdisplay, xid, + XInternAtom(xdisplay, "_KDE_NET_WM_BLUR_BEHIND_REGION", False)); + XFlush(xdisplay); +} +#endif /* GDK_WINDOWING_X11 */ -/* X11下依旧走原逻辑 本patch不应该生效 */ void gxde_set_blur_region(GdkWindow* window, int x, int y, int width, - int height) { - (void) window; - (void) x; - (void) y; - (void) width; - (void) height; + int height, int radius) { + if (window == NULL) { + return; + } +#ifdef GDK_WINDOWING_X11 + if (GDK_IS_X11_WINDOW(window)) { + x11_set_blur_region(window, x, y, width, height, radius); + return; + } +#endif +#ifdef GDK_WINDOWING_WAYLAND + wl_set_blur_region(window, x, y, width, height, radius); +#endif } void gxde_clear_blur(GdkWindow* window) { - (void) window; + if (window == NULL) { + return; + } +#ifdef GDK_WINDOWING_X11 + if (GDK_IS_X11_WINDOW(window)) { + x11_clear_blur(window); + return; + } +#endif +#ifdef GDK_WINDOWING_WAYLAND + wl_clear_blur(window); +#endif } +int gxde_blur_available(GdkDisplay* display) { + if (display == NULL) { + return 0; + } +#ifdef GDK_WINDOWING_X11 + if (GDK_IS_X11_DISPLAY(display)) { + return x11_blur_available(display); + } #endif +#ifdef GDK_WINDOWING_WAYLAND + return wl_blur_available(display); +#else + return 0; +#endif +} diff --git a/lib/wayland_blur.h b/lib/wayland_blur.h index 9fcfb15cbeae5c07c38dd30dcf3fd59465926001..5767f9af1939ad3a18a020c26f1fe5bae0f140fd 100644 --- a/lib/wayland_blur.h +++ b/lib/wayland_blur.h @@ -4,18 +4,23 @@ #include /* - * 在 Wayland 下通过 org_kde_kwin_blur 协议为窗口设置“背景模糊”区域, - * 对应 X11 上的 _KDE_NET_WM_BLUR_BEHIND_REGION 属性。 + * 为窗口设置“背景模糊”区域:Wayland 下走 org_kde_kwin_blur / Treeland 协议, + * X11 下写 _NET_WM_DEEPIN_BLUR_REGION_ROUNDED 与 _KDE_NET_WM_BLUR_BEHIND_REGION 属性。 * - * gxde_set_blur_region: 启用模糊,region 为 surface 逻辑坐标(无需乘缩放)。 + * gxde_set_blur_region: 启用模糊,region 为 surface 逻辑坐标(无需乘缩放), + * radius > 0 时四角按该半径做圆角,避免模糊区域在圆角外露出方角。 * gxde_clear_blur: 取消模糊。 + * gxde_blur_available: 查询合成器是否支持模糊。 * - * 传入的 window 必须已经 realize。非 Wayland 后端为 no-op,可安全无条件调用。 + * 传入的 window 必须已经 realize。 */ void gxde_set_blur_region(GdkWindow* window, int x, int y, int width, - int height); + int height, int radius); void gxde_clear_blur(GdkWindow* window); +/* 当前合成器是否支持背景模糊。 */ +int gxde_blur_available(GdkDisplay* display); + #endif /* LIB_WAYLAND_BLUR_H_ */ diff --git a/widget/appbar.vala b/widget/appbar.vala index e5ffcdf5cd38e1685ca474ff58b03c18dfeb0fbd..e4aea6b2add2e734ae1f4e9ce58b4239a57d7ea0 100644 --- a/widget/appbar.vala +++ b/widget/appbar.vala @@ -140,18 +140,10 @@ namespace Widgets { menu_content.append(new Menu.MenuItem("about", _("About"))); menu_content.append(new Menu.MenuItem("exit", _("Exit"))); - int menu_x, menu_y; - menu_button.translate_coordinates(menu_button.get_toplevel(), 0, 0, out menu_x, out menu_y); - Gtk.Allocation menu_rect; - menu_button.get_allocation(out menu_rect); - int window_x, window_y; - menu_button.get_toplevel().get_window().get_origin(out window_x, out window_y); - menu = new Menu.Menu(); menu.click_item.connect(handle_menu_item_click); menu.destroy.connect(handle_menu_destroy); - menu.set_prefer_deepin_menu(window.config.config_file.get_boolean("advanced", "prefer_deepin_menu")); - menu.popup_at_position(menu_content, window_x + menu_x, window_y + menu_y + menu_rect.height); + menu.popup_at_widget(menu_content, menu_button, Gdk.Gravity.SOUTH_WEST, Gdk.Gravity.NORTH_WEST, Gtk.get_current_event()); }); max_toggle_box = new Box(Gtk.Orientation.HORIZONTAL, 0); diff --git a/widget/entry.vala b/widget/entry.vala index 1e56d1fe30dc4bb2842bb91351324cd1c5be2b77..089367aae0d17f29269e264ac02cc886f32698a5 100644 --- a/widget/entry.vala +++ b/widget/entry.vala @@ -32,7 +32,7 @@ namespace Widgets { button_press_event.connect((w, e) => { if (Utils.is_right_button(e)) { menu = new Widgets.EntryMenu(); - menu.create_entry_menu(this, (int) e.x_root, (int) e.y_root); + menu.create_entry_menu(this, e); return true; } diff --git a/widget/entry_menu.vala b/widget/entry_menu.vala index d1dc8354f4511e0eb3a42ac982c156892345d216..9a8f3d1d5184ca4ebe914caa05f8d85aa6c4b3ca 100644 --- a/widget/entry_menu.vala +++ b/widget/entry_menu.vala @@ -32,7 +32,7 @@ namespace Widgets { Intl.bindtextdomain(GETTEXT_PACKAGE, "/usr/share/locale"); } - public void create_entry_menu(Gtk.Entry entry, int x, int y) { + public void create_entry_menu(Gtk.Entry entry, Gdk.Event event) { var menu_content = new List(); if (is_selection(entry)) { menu_content.append(new Menu.MenuItem("cut", _("Cut"))); @@ -51,7 +51,7 @@ namespace Widgets { handle_menu_item_click(entry, item_id); }); menu.destroy.connect(handle_menu_destroy); - menu.popup_at_position(menu_content, x, y); + menu.popup_at_pointer(menu_content, entry, event); } public void handle_menu_item_click(Gtk.Entry entry, string item_id) { diff --git a/widget/spinbutton.vala b/widget/spinbutton.vala index d892d1168b728f3fbe180b704ca04f8e5907f2f2..6a6d21d66e7c33c6cc316e4ce2d4c4066d7ee430 100644 --- a/widget/spinbutton.vala +++ b/widget/spinbutton.vala @@ -32,7 +32,7 @@ namespace Widgets { button_press_event.connect((w, e) => { if (Utils.is_right_button(e)) { menu = new Widgets.EntryMenu(); - menu.create_entry_menu(this, (int) e.x_root, (int) e.y_root); + menu.create_entry_menu(this, e); return true; } diff --git a/widget/tabbar.vala b/widget/tabbar.vala index 94e96a32ca83964e606d89592ac0e87df272b87d..1a1b18ecb6d5d365912a1ae5cb0872ed05e80f9f 100644 --- a/widget/tabbar.vala +++ b/widget/tabbar.vala @@ -292,7 +292,7 @@ namespace Widgets { } } - private void show_tab_menu(int x, int y, int tab_index, int tab_id) { + private void show_tab_menu(Gdk.Event event, int tab_index, int tab_id) { var menu_content = new GLib.List(); menu_content.append(new Menu.MenuItem("close_tab", _("Close tab"))); menu_content.append(new Menu.MenuItem("close_other_tabs", _("Close other tabs"), false, false, tab_list.size >= 2)); @@ -301,9 +301,7 @@ namespace Widgets { context_menu = new Menu.Menu(); context_menu.click_item.connect((item_id) => handle_tab_menu_click(item_id, tab_index, tab_id)); context_menu.destroy.connect(() => { context_menu = null; }); - var window = (Widgets.ConfigWindow) get_toplevel(); - context_menu.set_prefer_deepin_menu(window.config.config_file.get_boolean("advanced", "prefer_deepin_menu")); - context_menu.popup_at_position(menu_content, x, y); + context_menu.popup_at_pointer(menu_content, this, event); } private void handle_tab_menu_click(string item_id, int tab_index, int tab_id) { @@ -367,7 +365,7 @@ namespace Widgets { if (event.button == Gdk.BUTTON_SECONDARY) { int tab_index = get_tab_index_at_x((int) event.x); if (tab_index != -1) { - show_tab_menu((int) event.x_root, (int) event.y_root, tab_index, tab_list.get(tab_index)); + show_tab_menu(event, tab_index, tab_list.get(tab_index)); return true; } } diff --git a/widget/terminal.vala b/widget/terminal.vala index 2aae9e71bb5c16602ffaeea71fbd9c793c01293b..d4ed265ea459b7bf2c2b73e69adb2f2b52927ce1 100644 --- a/widget/terminal.vala +++ b/widget/terminal.vala @@ -191,7 +191,7 @@ namespace Widgets { focus_term(); uri_at_right_press = uri; - show_menu((int) event.x_root, (int) event.y_root); + show_menu(event); return false; } @@ -335,7 +335,8 @@ namespace Widgets { return in_remote_server; } - public void show_menu(int x, int y) { + // event 为空时在相对终端的 (x, y) 处弹出。 + public void show_menu(Gdk.Event? event, int x = 0, int y = 0) { bool in_quake_window = this.get_toplevel().get_type().is_a(typeof(Widgets.QuakeWindow)); // Set variable 'show_quake_menu' to true if terminal's window is quake window. @@ -460,12 +461,14 @@ namespace Widgets { menu_content.append(new Menu.MenuItem("", "")); menu_content.append(new Menu.MenuItem("preference", _("Settings"))); - var window = ((Widgets.Window) get_toplevel()); menu = new Menu.Menu(); menu.click_item.connect(handle_menu_item_click); menu.destroy.connect(handle_menu_destroy); - menu.set_prefer_deepin_menu(window.config.config_file.get_boolean("advanced", "prefer_deepin_menu")); - menu.popup_at_position(menu_content, x, y); + if (event != null) { + menu.popup_at_pointer(menu_content, term, event); + } else { + menu.popup_at_rect(menu_content, term, x, y, null); + } } @@ -877,24 +880,23 @@ namespace Widgets { Widgets.ConfigWindow parent_window = (Widgets.ConfigWindow) term.get_toplevel(); if (keyname == "Menu") { + // 用相对终端控件的指针位置(Wayland 下没有全局坐标),指针不在终端内时居中弹出。 int pointer_x, pointer_y; - Utils.get_pointer_position(out pointer_x, out pointer_y); - - int window_width, window_height; - ((ConfigWindow) get_toplevel()).get_size(out window_width, out window_height); + var pointer = term.get_display().get_default_seat().get_pointer(); + term.get_window().get_device_position(pointer, out pointer_x, out pointer_y, null); - int window_x, window_y; - ((ConfigWindow) get_toplevel()).get_window().get_origin(out window_x, out window_y); + Gtk.Allocation term_rect; + term.get_allocation(out term_rect); - if (pointer_x < window_x || pointer_x > window_x + window_width) { - pointer_x = window_x + window_width / 2; + if (pointer_x < 0 || pointer_x > term_rect.width) { + pointer_x = term_rect.width / 2; } - if (pointer_y < window_y || pointer_y > window_y + window_height) { - pointer_y = window_y + window_height / 2; + if (pointer_y < 0 || pointer_y > term_rect.height) { + pointer_y = term_rect.height / 2; } - show_menu(pointer_x, pointer_y); + show_menu(null, pointer_x, pointer_y); return true; } diff --git a/widget/window.vala b/widget/window.vala index f26a7c772b20775fe7d49e652bde7dd4ef417c9a..4095074f935a3acad397511c3458c5a296e363a0 100644 --- a/widget/window.vala +++ b/widget/window.vala @@ -37,7 +37,7 @@ namespace Widgets { // Blur配置 [CCode (cheader_filename = "wayland_blur.h", cname = "gxde_set_blur_region")] - private extern void wayland_set_blur_region (Gdk.Window window, int x, int y, int width, int height); + private extern void wayland_set_blur_region (Gdk.Window window, int x, int y, int width, int height, int radius); [CCode (cheader_filename = "wayland_blur.h", cname = "gxde_clear_blur")] private extern void wayland_clear_blur (Gdk.Window window); @@ -53,6 +53,7 @@ namespace Widgets { public int window_frame_margin_end = 50; public int window_frame_margin_start = 50; public int window_frame_margin_top = 50; + public int window_frame_radius = 8; public bool tabbar_at_the_bottom = false; public int window_fullscreen_monitor_height = Constant.TITLEBAR_HEIGHT * 2; public int window_fullscreen_monitor_timeout = 150; @@ -190,6 +191,23 @@ namespace Widgets { add(window_frame_box); window_frame_box.pack_start(window_widget_box, true, true, 0); + // GTK3不会按border-radius裁剪子控件,标题栏的方角会直接顶出来,这里强制裁剪 + window_widget_box.draw.connect((w, cr) => { + if (window_is_normal() && screen_monitor.is_composited()) { + Gtk.Allocation rect; + w.get_allocation(out rect); + Draw.clip_rounded_rectangle( + cr, + 1 - w.margin_start, + 1 - w.margin_top, + rect.width + w.margin_start + w.margin_end - 2, + rect.height + w.margin_top + w.margin_bottom - 2, + window_frame_radius - 1); + } + + return false; + }); + focus_in_event.connect((w) => { update_style(); @@ -267,9 +285,18 @@ namespace Widgets { draw_window_widgets(cr); + // 边框内侧的线条不能超出圆角 + cr.save(); + if (window_is_normal() && screen_monitor.is_composited()) { + Gtk.Allocation frame_rect; + window_frame_box.get_allocation(out frame_rect); + Draw.clip_rounded_rectangle(cr, frame_rect.x, frame_rect.y, frame_rect.width, frame_rect.height, window_frame_radius); + } + draw_window_frame(cr); draw_window_above(cr); + cr.restore(); return true; }); @@ -470,7 +497,8 @@ namespace Widgets { h = height; } - wayland_set_blur_region(get_window(), x, y, w, h); + int radius = (window_is_normal() && screen_monitor.is_composited()) ? window_frame_radius : 0; + wayland_set_blur_region(get_window(), x, y, w, h, radius); } } catch (GLib.KeyFileError e) { print("%s\n", e.message); @@ -638,6 +666,9 @@ namespace Widgets { if (tabbar_at_the_bottom) { titlebar_y += height - Constant.TITLEBAR_HEIGHT; } + + // 标题栏竖线需要在Wayland HDPI下对齐 + int titlebar_side_y = titlebar_y; if (get_scale_factor() > 1) { titlebar_y -= 1; } @@ -694,9 +725,9 @@ namespace Widgets { // Draw line around titlebar side. cr.set_source_rgba(frame_color.red, frame_color.green, frame_color.blue, config.config_file.get_double("general", "opacity")); // Left. - Draw.draw_rectangle(cr, x + 1, titlebar_y + side_margin, 1, Constant.TITLEBAR_HEIGHT); + Draw.draw_rectangle(cr, x + 1, titlebar_side_y + side_margin, 1, Constant.TITLEBAR_HEIGHT); // Right. - Draw.draw_rectangle(cr, x + width - 2, titlebar_y + side_margin, 1, Constant.TITLEBAR_HEIGHT); + Draw.draw_rectangle(cr, x + width - 2, titlebar_side_y + side_margin, 1, Constant.TITLEBAR_HEIGHT); // if (is_light_theme) { // Utils.set_context_color(cr, top_line_light_color); @@ -705,9 +736,9 @@ namespace Widgets { // } // Left. - Draw.draw_rectangle(cr, x + 1, titlebar_y + side_margin, 1, Constant.TITLEBAR_HEIGHT); + Draw.draw_rectangle(cr, x + 1, titlebar_side_y + side_margin, 1, Constant.TITLEBAR_HEIGHT); // Right. - Draw.draw_rectangle(cr, x + width - 2, titlebar_y + side_margin, 1, Constant.TITLEBAR_HEIGHT); + Draw.draw_rectangle(cr, x + width - 2, titlebar_side_y + side_margin, 1, Constant.TITLEBAR_HEIGHT); if (tabbar_at_the_bottom) { draw_titlebar_underline(cr, x + 1, titlebar_y, width - 2, -1);