[gnome-games/wip/exalm/unified-window: 7/7] Something



commit 5f0f36f5ddd73c748ed29249ad57caa18182cf03
Author: Alexander Mikhaylenko <alexm gnome org>
Date:   Mon Mar 16 14:46:54 2020 +0500

    Something

 data/gtk-style.css                       |  20 ++
 data/ui/application-window.ui            |  23 ++-
 data/ui/collection-header-bar.ui         |   6 +
 data/ui/display-header-bar.ui            |   6 +
 data/ui/preferences-subpage-gamepad.ui   |  87 ++++----
 data/ui/preferences-subpage-keyboard.ui  |  87 ++++----
 data/ui/preferences-window.ui            | 253 +++++++++++------------
 src/main.vala                            |   1 +
 src/meson.build                          |   3 +
 src/titlebar.vala                        | 345 +++++++++++++++++++++++++++++++
 src/ui/application-window.vala           |   2 +-
 src/ui/application.vala                  |   2 +-
 src/ui/preferences-subpage-gamepad.vala  |  15 +-
 src/ui/preferences-subpage-keyboard.vala |  15 +-
 src/ui/preferences-subpage.vala          |   2 -
 src/ui/preferences-window.vala           |  60 +++---
 src/unified-window.vala                  | 245 ++++++++++++++++++++++
 17 files changed, 894 insertions(+), 278 deletions(-)
---
diff --git a/data/gtk-style.css b/data/gtk-style.css
index 77d4ac32..965ffcf7 100644
--- a/data/gtk-style.css
+++ b/data/gtk-style.css
@@ -111,3 +111,23 @@ list.rounded:backdrop,
 list.separators row:not(:last-child) {
        border-bottom: 1px solid rgba(0, 0, 0, 0.15);
 }
+
+window.unified:not(.tiled):not(.tiled-top):not(.tiled-bottom):not(.tiled-left):not(.tiled-right):not(.maximized):not(.fullscreen):not(.solid-csd),
+window.unified:not(.tiled):not(.tiled-top):not(.tiled-bottom):not(.tiled-left):not(.tiled-right):not(.maximized):not(.fullscreen):not(.solid-csd)
decoration,
+window.unified:not(.tiled):not(.tiled-top):not(.tiled-bottom):not(.tiled-left):not(.tiled-right):not(.maximized):not(.fullscreen):not(.solid-csd)
decoration-overlay {
+  border-radius: 8px;
+}
+
+window.unified > decoration-overlay {
+  box-shadow: inset 0 1px alpha(white, 0.065);
+}
+
+window.unified headerbar {
+  border-radius: 0;
+  box-shadow: none;
+}
+
+titlebar, titlebar * {
+  -GtkWidget-window-dragging: true;
+}
+
diff --git a/data/ui/application-window.ui b/data/ui/application-window.ui
index c5d87266..20fe5115 100644
--- a/data/ui/application-window.ui
+++ b/data/ui/application-window.ui
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <interface>
   <requires lib="gtk+" version="3.24"/>
-  <template class="GamesApplicationWindow" parent="GtkApplicationWindow">
+  <template class="GamesApplicationWindow" parent="GamesUnifiedWindow">
     <property name="default-width">768</property>
     <property name="default-height">600</property>
     <property name="show-menubar">False</property>
@@ -11,16 +11,23 @@
     <signal name="window-state-event" after="yes" handler="on_window_state_event"/>
     <signal name="notify::is-active" after="yes" handler="on_active_changed"/>
     <child>
-      <object class="GtkStack" id="content_box">
-        <property name="visible">True</property>
-      </object>
-    </child>
-    <child type="titlebar">
-      <object class="HdyTitleBar">
+      <object class="GtkBox">
         <property name="visible">True</property>
+        <property name="orientation">vertical</property>
+        <child>
+          <object class="GamesTitleBar">
+            <property name="visible">True</property>
+            <child>
+              <object class="GtkStack" id="header_bar">
+                <property name="visible">True</property>
+              </object>
+            </child>
+          </object>
+        </child>
         <child>
-          <object class="GtkStack" id="header_bar">
+          <object class="GtkStack" id="content_box">
             <property name="visible">True</property>
+            <property name="expand">True</property>
           </object>
         </child>
       </object>
diff --git a/data/ui/collection-header-bar.ui b/data/ui/collection-header-bar.ui
index 73faac3a..9893b159 100644
--- a/data/ui/collection-header-bar.ui
+++ b/data/ui/collection-header-bar.ui
@@ -13,6 +13,9 @@
             <property name="visible">True</property>
             <property name="show-close-button">True</property>
             <property name="centering-policy">strict</property>
+            <style>
+              <class name="titlebar"/>
+            </style>
             <child>
               <object class="GtkButton" id="add_game">
                 <property name="visible">True</property>
@@ -101,6 +104,9 @@
             <property name="visible">True</property>
             <property name="show_close_button">True</property>
             <property name="title" bind-source="GamesCollectionHeaderBar" bind-property="subview-title" 
bind-flags="bidirectional"/>
+            <style>
+              <class name="titlebar"/>
+            </style>
             <child>
               <object class="GtkButton">
                 <property name="visible">True</property>
diff --git a/data/ui/display-header-bar.ui b/data/ui/display-header-bar.ui
index 41412b77..03055726 100644
--- a/data/ui/display-header-bar.ui
+++ b/data/ui/display-header-bar.ui
@@ -16,6 +16,9 @@
             <property name="visible">True</property>
             <property name="title" bind-source="GamesDisplayHeaderBar" bind-property="game-title" 
bind-flags="bidirectional"/>
             <property name="show-close-button" bind-source="GamesDisplayHeaderBar" 
bind-property="show-title-buttons" bind-flags="bidirectional|sync-create"/>
+            <style>
+              <class name="titlebar"/>
+            </style>
             <child>
               <object class="GtkButton" id="back">
                 <property name="visible">True</property>
@@ -135,6 +138,9 @@
           <object class="GtkHeaderBar" id="snapshots_header_bar">
             <property name="visible">True</property>
             <property name="title" bind-source="GamesDisplayHeaderBar" bind-property="game-title" 
bind-flags="bidirectional"/>
+            <style>
+              <class name="titlebar"/>
+            </style>
             <child>
               <object class="GtkButton">
                 <property name="sensitive">False</property>
diff --git a/data/ui/preferences-subpage-gamepad.ui b/data/ui/preferences-subpage-gamepad.ui
index ed70871b..f915b9fc 100644
--- a/data/ui/preferences-subpage-gamepad.ui
+++ b/data/ui/preferences-subpage-gamepad.ui
@@ -4,6 +4,55 @@
   <template class="GamesPreferencesSubpageGamepad" parent="GtkBox">
     <property name="visible">True</property>
     <property name="orientation">vertical</property>
+    <child>
+      <object class="GamesTitleBar">
+        <property name="visible">True</property>
+        <property name="vexpand">False</property>
+        <child>
+          <object class="GtkHeaderBar" id="header_bar">
+            <property name="visible">True</property>
+            <property name="expand">True</property>
+            <style>
+              <class name="titlebar"/>
+            </style>
+            <child>
+              <object class="GtkButton" id="back_button">
+                <property name="visible">True</property>
+                <signal name="clicked" handler="on_back_clicked"/>
+                <style>
+                  <class name="image-button"/>
+                </style>
+                <child internal-child="accessible">
+                  <object class="AtkObject" id="a11y-back">
+                    <property name="accessible-name" translatable="yes">Back</property>
+                  </object>
+                </child>
+                <child>
+                  <object class="GtkImage" id="back_image">
+                    <property name="visible">True</property>
+                    <property name="icon-name">go-previous-symbolic</property>
+                    <property name="icon-size">1</property>
+                  </object>
+                </child>
+              </object>
+              <packing>
+                <property name="pack-type">start</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkButton" id="cancel_button">
+                <property name="visible">True</property>
+                <property name="label" translatable="yes">Cancel</property>
+                <signal name="clicked" handler="on_cancel_clicked"/>
+              </object>
+              <packing>
+                <property name="pack-type">end</property>
+              </packing>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
     <child>
       <object class="GtkStack" id="gamepad_view_stack">
         <property name="visible">True</property>
@@ -69,42 +118,4 @@
       </object>
     </child>
   </template>
-  <object class="GtkHeaderBar" id="header_bar">
-    <property name="visible">True</property>
-    <property name="expand">True</property>
-    <child>
-      <object class="GtkButton" id="back_button">
-        <property name="visible">True</property>
-        <signal name="clicked" handler="on_back_clicked"/>
-        <style>
-          <class name="image-button"/>
-        </style>
-        <child internal-child="accessible">
-          <object class="AtkObject" id="a11y-back">
-            <property name="accessible-name" translatable="yes">Back</property>
-          </object>
-        </child>
-        <child>
-          <object class="GtkImage" id="back_image">
-            <property name="visible">True</property>
-            <property name="icon-name">go-previous-symbolic</property>
-            <property name="icon-size">1</property>
-          </object>
-        </child>
-      </object>
-      <packing>
-        <property name="pack-type">start</property>
-      </packing>
-    </child>
-    <child>
-      <object class="GtkButton" id="cancel_button">
-        <property name="visible">True</property>
-        <property name="label" translatable="yes">Cancel</property>
-        <signal name="clicked" handler="on_cancel_clicked"/>
-      </object>
-      <packing>
-        <property name="pack-type">end</property>
-      </packing>
-    </child>
-  </object>
 </interface>
diff --git a/data/ui/preferences-subpage-keyboard.ui b/data/ui/preferences-subpage-keyboard.ui
index 17675b8a..14143a07 100644
--- a/data/ui/preferences-subpage-keyboard.ui
+++ b/data/ui/preferences-subpage-keyboard.ui
@@ -5,6 +5,55 @@
     <property name="visible">True</property>
     <property name="can-focus">True</property>
     <property name="orientation">vertical</property>
+    <child>
+      <object class="GamesTitleBar">
+        <property name="visible">True</property>
+        <property name="vexpand">False</property>
+        <child>
+          <object class="GtkHeaderBar" id="header_bar">
+            <property name="visible">True</property>
+            <property name="expand">True</property>
+            <style>
+              <class name="titlebar"/>
+            </style>
+            <child>
+              <object class="GtkButton" id="back_button">
+                <property name="visible">True</property>
+                <signal name="clicked" handler="on_back_clicked"/>
+                <style>
+                  <class name="image-button"/>
+                </style>
+                <child internal-child="accessible">
+                  <object class="AtkObject" id="a11y-back">
+                    <property name="accessible-name" translatable="yes">Back</property>
+                  </object>
+                </child>
+                <child>
+                  <object class="GtkImage" id="back_image">
+                    <property name="visible">True</property>
+                    <property name="icon-name">go-previous-symbolic</property>
+                    <property name="icon-size">1</property>
+                  </object>
+                </child>
+              </object>
+              <packing>
+                <property name="pack-type">start</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkButton" id="cancel_button">
+                <property name="visible">True</property>
+                <property name="label" translatable="yes">Cancel</property>
+                <signal name="clicked" handler="on_cancel_clicked"/>
+              </object>
+              <packing>
+                <property name="pack-type">end</property>
+              </packing>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
     <child>
       <object class="GtkStack" id="gamepad_view_stack">
         <property name="visible">True</property>
@@ -70,42 +119,4 @@
       </object>
     </child>
   </template>
-  <object class="GtkHeaderBar" id="header_bar">
-    <property name="visible">True</property>
-    <property name="expand">True</property>
-    <child>
-      <object class="GtkButton" id="back_button">
-        <property name="visible">True</property>
-        <signal name="clicked" handler="on_back_clicked"/>
-        <style>
-          <class name="image-button"/>
-        </style>
-        <child internal-child="accessible">
-          <object class="AtkObject" id="a11y-back">
-            <property name="accessible-name" translatable="yes">Back</property>
-          </object>
-        </child>
-        <child>
-          <object class="GtkImage" id="back_image">
-            <property name="visible">True</property>
-            <property name="icon-name">go-previous-symbolic</property>
-            <property name="icon-size">1</property>
-          </object>
-        </child>
-      </object>
-      <packing>
-        <property name="pack-type">start</property>
-      </packing>
-    </child>
-    <child>
-      <object class="GtkButton" id="cancel_button">
-        <property name="visible">True</property>
-        <property name="label" translatable="yes">Cancel</property>
-        <signal name="clicked" handler="on_cancel_clicked"/>
-      </object>
-      <packing>
-        <property name="pack-type">end</property>
-      </packing>
-    </child>
-  </object>
 </interface>
diff --git a/data/ui/preferences-window.ui b/data/ui/preferences-window.ui
index 2a6ca3d1..c33174f3 100644
--- a/data/ui/preferences-window.ui
+++ b/data/ui/preferences-window.ui
@@ -1,126 +1,80 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <interface>
   <requires lib="gtk+" version="3.24"/>
-  <template class="GamesPreferencesWindow" parent="GtkWindow">
+  <template class="GamesPreferencesWindow" parent="GamesUnifiedWindow">
     <property name="title" translatable="yes">Preferences</property>
     <property name="default-width">800</property>
     <property name="default-height">500</property>
     <property name="window-position">center-on-parent</property>
-    <child type="titlebar">
-      <object class="HdyTitleBar" id="titlebar">
+    <child>
+      <object class="HdyDeck" id="deck">
         <property name="visible">True</property>
+        <property name="can-swipe-back">True</property>
+        <property name="expand">True</property>
+        <signal name="notify::transition-running" handler="subpage_transition_finished"/>
         <child>
-          <object class="HdyDeck" id="titlebar_deck">
+          <object class="HdyLeaflet" id="leaflet">
             <property name="visible">True</property>
+            <property name="can-swipe-back">True</property>
+            <signal name="notify::visible-child" handler="update_header_group"/>
+            <signal name="notify::folded" handler="on_folded_changed" after="yes"/>
             <child>
-              <object class="HdyLeaflet" id="titlebar_leaflet">
+              <object class="GtkBox">
                 <property name="visible">True</property>
-                <signal name="notify::visible-child" handler="update_header_group"/>
+                <property name="orientation">vertical</property>
                 <child>
-                  <object class="GtkHeaderBar" id="left_header_bar">
-                    <property name="name">left_header_bar</property>
+                  <object class="GamesTitleBar">
                     <property name="visible">True</property>
-                    <property name="title" translatable="yes">Preferences</property>
-                    <property name="show-close-button">True</property>
                     <child>
-                      <object class="GtkButton" id="window_back_button">
-                        <property name="visible">False</property>
-                        <signal name="clicked" handler="on_back_clicked"/>
+                      <object class="GtkHeaderBar" id="left_header_bar">
+                        <property name="name">left_header_bar</property>
+                        <property name="visible">True</property>
+                        <property name="title" translatable="yes">Preferences</property>
+                        <property name="show-close-button">True</property>
                         <style>
-                          <class name="image-button"/>
+                          <class name="titlebar"/>
                         </style>
-                        <child internal-child="accessible">
-                          <object class="AtkObject">
-                            <property name="accessible-name" translatable="yes">Back</property>
-                          </object>
-                        </child>
                         <child>
-                          <object class="GtkImage">
-                            <property name="visible">True</property>
-                            <property name="icon-name">go-previous-symbolic</property>
-                            <property name="icon-size">1</property>
+                          <object class="GtkButton" id="window_back_button">
+                            <property name="visible">False</property>
+                            <signal name="clicked" handler="on_back_clicked"/>
+                            <style>
+                              <class name="image-button"/>
+                            </style>
+                            <child internal-child="accessible">
+                              <object class="AtkObject">
+                                <property name="accessible-name" translatable="yes">Back</property>
+                              </object>
+                            </child>
+                            <child>
+                              <object class="GtkImage">
+                                <property name="visible">True</property>
+                                <property name="icon-name">go-previous-symbolic</property>
+                                <property name="icon-size">1</property>
+                              </object>
+                            </child>
                           </object>
+                          <packing>
+                            <property name="pack-type">start</property>
+                          </packing>
                         </child>
                       </object>
-                      <packing>
-                        <property name="pack-type">start</property>
-                      </packing>
                     </child>
                   </object>
                 </child>
                 <child>
-                  <object class="GtkSeparator" id="header_separator">
-                    <property name="orientation">vertical</property>
+                  <object class="GamesPreferencesSidebar" id="sidebar">
+                    <property name="stack">stack</property>
+                    <property name="vexpand">True</property>
                     <property name="visible">True</property>
-                    <style>
-                      <class name="sidebar"/>
-                    </style>
+                    <property name="width-request">150</property>
+                    <signal name="row-selected" handler="sidebar_row_selected"/>
                   </object>
-                  <packing>
-                    <property name="allow-visible">False</property>
-                  </packing>
                 </child>
-                <child>
-                  <object class="GtkHeaderBar" id="right_header_bar">
-                    <property name="name">right_header_bar</property>
-                    <property name="visible">True</property>
-                    <property name="hexpand">True</property>
-                    <property name="show-close-button">True</property>
-                    <child>
-                      <object class="GtkButton" id="page_back_button">
-                        <property name="visible">False</property>
-                        <signal name="clicked" handler="on_back_clicked"/>
-                        <style>
-                          <class name="image-button"/>
-                        </style>
-                        <child internal-child="accessible">
-                          <object class="AtkObject">
-                            <property name="accessible-name" translatable="yes">Back</property>
-                          </object>
-                        </child>
-                        <child>
-                          <object class="GtkImage">
-                            <property name="visible">True</property>
-                            <property name="icon-name">go-previous-symbolic</property>
-                            <property name="icon-size">1</property>
-                          </object>
-                        </child>
-                      </object>
-                      <packing>
-                        <property name="pack-type">start</property>
-                      </packing>
-                    </child>
-                  </object>
-                </child>
-              </object>
-            </child>
-            <child>
-              <object class="GtkBox" id="titlebar_subpage_box">
-                <property name="visible">True</property>
-              </object>
-            </child>
-          </object>
-        </child>
-      </object>
-    </child>
-    <child>
-      <object class="HdyDeck" id="content_deck">
-        <property name="visible">True</property>
-        <property name="can-swipe-back" bind-source="titlebar" bind-property="selection-mode" 
bind-flags="sync-create|invert-boolean"/>
-        <signal name="notify::transition-running" handler="subpage_transition_finished"/>
-        <child>
-          <object class="HdyLeaflet" id="content_leaflet">
-            <property name="visible">True</property>
-            <property name="can-swipe-back">True</property>
-            <signal name="notify::folded" handler="on_folded_changed" after="yes"/>
-            <child>
-              <object class="GamesPreferencesSidebar" id="sidebar">
-                <property name="stack">stack</property>
-                <property name="vexpand">True</property>
-                <property name="visible">True</property>
-                <property name="width-request">150</property>
-                <signal name="row-selected" handler="sidebar_row_selected"/>
               </object>
+              <packing>
+                <property name="name">sidebar</property>
+              </packing>
             </child>
             <child>
               <object class="GtkSeparator" id="separator">
@@ -135,38 +89,87 @@
               </packing>
             </child>
             <child>
-              <object class="GtkStack" id="stack">
+              <object class="GtkBox">
                 <property name="visible">True</property>
-                <property name="expand">True</property>
-                <property name="visible-child">video_page</property>
-                <property name="transition-type">crossfade</property>
-                <property name="width-request">300</property>
-                <child>
-                  <object class="GamesPreferencesPageVideo" id="video_page">
-                    <property name="visible">True</property>
-                  </object>
-                </child>
-                <child>
-                  <object class="GamesPreferencesPageControllers" id="controllers_page">
-                    <property name="visible">True</property>
-                  </object>
-                </child>
+                <property name="orientation">vertical</property>
                 <child>
-                  <object class="GamesPreferencesPagePlatforms" id="platforms_page">
+                  <object class="GamesTitleBar">
                     <property name="visible">True</property>
+                    <child>
+                      <object class="GtkHeaderBar" id="right_header_bar">
+                        <property name="name">right_header_bar</property>
+                        <property name="visible">True</property>
+                        <property name="hexpand">True</property>
+                        <property name="show-close-button">True</property>
+                        <style>
+                          <class name="titlebar"/>
+                        </style>
+                        <child>
+                          <object class="GtkButton" id="page_back_button">
+                            <property name="visible">False</property>
+                            <signal name="clicked" handler="on_back_clicked"/>
+                            <style>
+                              <class name="image-button"/>
+                            </style>
+                            <child internal-child="accessible">
+                              <object class="AtkObject">
+                                <property name="accessible-name" translatable="yes">Back</property>
+                              </object>
+                            </child>
+                            <child>
+                              <object class="GtkImage">
+                                <property name="visible">True</property>
+                                <property name="icon-name">go-previous-symbolic</property>
+                                <property name="icon-size">1</property>
+                              </object>
+                            </child>
+                          </object>
+                          <packing>
+                            <property name="pack-type">start</property>
+                          </packing>
+                        </child>
+                      </object>
+                    </child>
                   </object>
                 </child>
                 <child>
-                  <object class="GamesPreferencesPageImportExport" id="import_export_page">
+                  <object class="GtkStack" id="stack">
                     <property name="visible">True</property>
+                    <property name="expand">True</property>
+                    <property name="visible-child">video_page</property>
+                    <property name="transition-type">crossfade</property>
+                    <property name="width-request">300</property>
+                    <child>
+                      <object class="GamesPreferencesPageVideo" id="video_page">
+                        <property name="visible">True</property>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GamesPreferencesPageControllers" id="controllers_page">
+                        <property name="visible">True</property>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GamesPreferencesPagePlatforms" id="platforms_page">
+                        <property name="visible">True</property>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GamesPreferencesPageImportExport" id="import_export_page">
+                        <property name="visible">True</property>
+                      </object>
+                    </child>
                   </object>
                 </child>
               </object>
+              <packing>
+                <property name="name">content</property>
+              </packing>
             </child>
           </object>
         </child>
         <child>
-          <object class="GtkBox" id="content_subpage_box">
+          <object class="GtkBox" id="subpage_box">
             <property name="visible">True</property>
             <property name="orientation">vertical</property>
           </object>
@@ -174,36 +177,10 @@
       </object>
     </child>
   </template>
-  <object class="GtkSizeGroup">
-    <property name="mode">horizontal</property>
-    <widgets>
-      <widget name="left_header_bar"/>
-      <widget name="sidebar"/>
-    </widgets>
-  </object>
-  <object class="GtkSizeGroup">
-    <property name="mode">horizontal</property>
-    <widgets>
-      <widget name="right_header_bar"/>
-      <widget name="stack"/>
-    </widgets>
-  </object>
   <object class="HdyHeaderGroup" id="header_group">
     <headerbars>
       <headerbar name="left_header_bar"/>
       <headerbar name="right_header_bar"/>
     </headerbars>
   </object>
-  <object class="HdySwipeGroup">
-    <swipeables>
-      <swipeable name="titlebar_deck"/>
-      <swipeable name="content_deck"/>
-    </swipeables>
-  </object>
-  <object class="HdySwipeGroup">
-    <swipeables>
-      <swipeable name="titlebar_leaflet"/>
-      <swipeable name="content_leaflet"/>
-    </swipeables>
-  </object>
 </interface>
diff --git a/src/main.vala b/src/main.vala
index ee72763e..20cd8839 100644
--- a/src/main.vala
+++ b/src/main.vala
@@ -7,6 +7,7 @@ int main (string[] args) {
 
        string[] empty_args = {};
        unowned var unowned_args = empty_args;
+       typeof (Games.TitleBar).ensure ();
 
        Grl.init (ref unowned_args);
 
diff --git a/src/meson.build b/src/meson.build
index 14445037..13d32da1 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -177,6 +177,9 @@ vala_sources = [
 
   'credits.vala',
   'main.vala',
+
+  'titlebar.vala',
+  'unified-window.vala',
 ]
 
 event_codes_dep = valac.find_library ('event-codes', dirs: gamepad_dir)
diff --git a/src/titlebar.vala b/src/titlebar.vala
new file mode 100644
index 00000000..e94ad150
--- /dev/null
+++ b/src/titlebar.vala
@@ -0,0 +1,345 @@
+public class Games.TitleBar : Gtk.EventBox {
+    private Gtk.GestureMultiPress multipress_gesture;
+    private Gtk.Menu? fallback_menu;
+    private bool keep_above;
+
+    static construct {
+        set_css_name ("titlebar");
+    }
+
+    construct {
+        multipress_gesture = new Gtk.GestureMultiPress (this);
+        multipress_gesture.set_button (0);
+        multipress_gesture.set_propagation_phase (Gtk.PropagationPhase.NONE);
+        multipress_gesture.pressed.connect (pressed_cb);
+
+        add_events (
+            Gdk.EventMask.BUTTON_PRESS_MASK |
+            Gdk.EventMask.BUTTON_RELEASE_MASK |
+            Gdk.EventMask.BUTTON_MOTION_MASK |
+            Gdk.EventMask.TOUCH_MASK
+        );
+    }
+
+    protected override bool event (Gdk.Event event) {
+        if (multipress_gesture == null)
+            return Gdk.EVENT_PROPAGATE;
+
+        var type = event.type;
+        if (type != Gdk.EventType.BUTTON_PRESS &&
+            type != Gdk.EventType.BUTTON_RELEASE &&
+            type != Gdk.EventType.MOTION_NOTIFY &&
+            type != Gdk.EventType.TOUCH_BEGIN &&
+            type != Gdk.EventType.TOUCH_END &&
+            type != Gdk.EventType.TOUCH_UPDATE)
+            return Gdk.EVENT_PROPAGATE;
+
+        var sequence = event.get_event_sequence ();
+        var retval = multipress_gesture.handle_event (event);
+
+        /* Reset immediately the gestures, here we don't get many guarantees
+         * about whether the target window event mask will be complete enough
+         * to keep gestures consistent, or whether any widget across the
+         * hierarchy will be inconsistent about event handler return values.
+         */
+        if (multipress_gesture.get_sequence_state (sequence) == Gtk.EventSequenceState.DENIED)
+            multipress_gesture.reset ();
+
+        return retval;
+    }
+
+    private void pressed_cb (int n_press, double x, double y) {
+        var sequence = multipress_gesture.get_current_sequence ();
+        var button = multipress_gesture.get_current_button ();
+        var event = multipress_gesture.get_last_event (sequence);
+
+        if (event == null)
+            return;
+
+        if (get_display ().device_is_grabbed (multipress_gesture.get_device ()))
+            return;
+
+        switch (button) {
+            case Gdk.BUTTON_PRIMARY:
+                get_toplevel ().get_window ().raise ();
+
+                if (n_press == 2)
+                    titlebar_action (event, button, n_press);
+
+                if (has_grab ())
+                    multipress_gesture.set_sequence_state (sequence, Gtk.EventSequenceState.CLAIMED);
+
+                break;
+
+            case Gdk.BUTTON_SECONDARY:
+                if (titlebar_action (event, button, n_press))
+                    multipress_gesture.set_sequence_state (sequence, Gtk.EventSequenceState.CLAIMED);
+
+                multipress_gesture.reset ();
+
+                break;
+
+            case Gdk.BUTTON_MIDDLE:
+                if (titlebar_action (event, button, n_press))
+                    multipress_gesture.set_sequence_state (sequence, Gtk.EventSequenceState.CLAIMED);
+
+                break;
+        }
+    }
+
+    private bool titlebar_action (Gdk.Event event, uint button, int n_press) {
+        var settings = get_settings ();
+        string? action = null;
+
+        switch (button) {
+            case Gdk.BUTTON_PRIMARY:
+                if (n_press == 2)
+                    action = settings.gtk_titlebar_double_click;
+                break;
+
+            case Gdk.BUTTON_MIDDLE:
+                action = settings.gtk_titlebar_middle_click;
+                break;
+
+            case Gdk.BUTTON_SECONDARY:
+                action = settings.gtk_titlebar_right_click;
+                break;
+        }
+
+        if (action == null)
+            return false;
+
+        if (action == "none")
+            return false;
+
+        var toplevel = get_toplevel ();
+        if (!(toplevel is Gtk.Window))
+            return false;
+
+        /* treat all maximization variants the same */
+        if (action.has_prefix ("toggle-maximize")) {
+            /*
+             * gtk header bar won't show the maximize button if the following
+             * properties are not met, apply the same to title bar actions for
+             * consistency.
+             */
+            var toplevel_window = toplevel as Gtk.Window;
+            if (toplevel_window.resizable &&
+                toplevel_window.type_hint == Gdk.WindowTypeHint.NORMAL)
+                toggle_maximized ();
+            return true;
+        }
+
+        if (action == "lower") {
+            toplevel.get_window ().lower ();
+            return true;
+        }
+
+        if (action == "minimize") {
+            toplevel.get_window ().iconify ();
+            return true;
+        }
+
+        if (action == "menu") {
+            do_popup (event);
+            return true;
+        }
+
+        warning ("Unsupported titlebar action %s", action);
+        return false;
+    }
+
+    private void toggle_maximized () {
+        var toplevel = get_toplevel ();
+        if (!(toplevel is Gtk.Window))
+            return;
+
+        var toplevel_window = toplevel as Gtk.Window;
+
+        if (toplevel_window.is_maximized)
+            toplevel_window.unmaximize ();
+        else
+            toplevel_window.maximize ();
+    }
+
+    private void do_popup (Gdk.Event event) {
+        var toplevel = get_toplevel ();
+        if (!(toplevel is Gtk.Window))
+            return;
+
+        if (toplevel.get_window ().show_window_menu (event))
+            return;
+
+        var toplevel_window = toplevel as Gtk.Window;
+
+        if (fallback_menu != null)
+            fallback_menu.destroy ();
+
+        var state = toplevel.get_window ().get_state ();
+        var iconified = (state & Gdk.WindowState.ICONIFIED) > 0;
+        var maximized = toplevel_window.is_maximized && !iconified;
+
+        fallback_menu = new Gtk.Menu ();
+        fallback_menu.get_style_context ().add_class (Gtk.STYLE_CLASS_CONTEXT_MENU);
+
+        fallback_menu.attach_to_widget (this, (widget) => {
+            var self = widget as TitleBar;
+            self.fallback_menu = null;
+        });
+
+        var menuitem = new Gtk.MenuItem.with_label (_("Restore"));
+        menuitem.show ();
+
+        /* "Restore" means "Unmaximize" or "Unminimize"
+         * (yes, some WMs allow window menu to be shown for minimized windows).
+         * Not restorable:
+         *   - visible windows that are not maximized or minimized
+         *   - non-resizable windows that are not minimized
+         *   - non-normal windows
+         */
+        if ((toplevel_window.is_visible () && !(maximized || iconified)) ||
+            (!iconified && !toplevel_window.resizable) ||
+            toplevel_window.type_hint != Gdk.WindowTypeHint.NORMAL)
+            menuitem.sensitive = false;
+
+        menuitem.activate.connect (restore_window_clicked);
+        fallback_menu.append (menuitem);
+
+        menuitem = new Gtk.MenuItem.with_label (_("Move"));
+        menuitem.show ();
+        if (maximized || iconified)
+            menuitem.sensitive = false;
+        menuitem.activate.connect (move_window_clicked);
+        fallback_menu.append (menuitem);
+
+        menuitem = new Gtk.MenuItem.with_label (_("Resize"));
+        menuitem.show ();
+        if (!toplevel_window.resizable || maximized || iconified)
+            menuitem.sensitive = false;
+        menuitem.activate.connect (resize_window_clicked);
+        fallback_menu.append (menuitem);
+
+        menuitem = new Gtk.MenuItem.with_label (_("Minimize"));
+        menuitem.show ();
+        if (iconified || toplevel_window.type_hint != Gdk.WindowTypeHint.NORMAL)
+            menuitem.sensitive = false;
+        menuitem.activate.connect (minimize_window_clicked);
+        fallback_menu.append (menuitem);
+
+        menuitem = new Gtk.MenuItem.with_label (_("Maximize"));
+        menuitem.show ();
+        if (maximized || !toplevel_window.resizable ||
+            toplevel_window.type_hint != Gdk.WindowTypeHint.NORMAL)
+            menuitem.sensitive = false;
+        menuitem.activate.connect (maximize_window_clicked);
+        fallback_menu.append (menuitem);
+
+        menuitem = new Gtk.SeparatorMenuItem ();
+        menuitem.show ();
+        fallback_menu.append (menuitem);
+
+        menuitem = new Gtk.CheckMenuItem.with_label (_("Always on Top"));
+        ((Gtk.CheckMenuItem) menuitem).active = keep_above;
+        if (maximized)
+            menuitem.sensitive = false;
+        menuitem.show ();
+        menuitem.activate.connect (ontop_window_clicked);
+        fallback_menu.append (menuitem);
+
+        menuitem = new Gtk.SeparatorMenuItem ();
+        menuitem.show ();
+        fallback_menu.append (menuitem);
+
+        menuitem = new Gtk.MenuItem.with_label (_("Close"));
+        menuitem.show ();
+        if (!toplevel_window.deletable)
+            menuitem.sensitive = false;
+        menuitem.activate.connect (close_window_clicked);
+        fallback_menu.append (menuitem);
+
+        fallback_menu.popup_at_pointer (event);
+    }
+
+    private void restore_window_clicked () {
+        var toplevel_window = get_toplevel () as Gtk.Window;
+        if (toplevel_window == null)
+            return;
+
+        if (toplevel_window.is_maximized) {
+            toplevel_window.unmaximize ();
+            return;
+        }
+
+        var state = toplevel_window.get_window ().get_state ();
+        if ((state & Gdk.WindowState.ICONIFIED) > 0)
+            toplevel_window.deiconify ();
+    }
+
+    private void move_window_clicked () {
+        var toplevel_window = get_toplevel () as Gtk.Window;
+        if (toplevel_window == null)
+            return;
+
+        toplevel_window.begin_move_drag (0, 0, 0, Gdk.CURRENT_TIME);
+    }
+
+    private void resize_window_clicked () {
+        var toplevel_window = get_toplevel () as Gtk.Window;
+        if (toplevel_window == null)
+            return;
+
+        toplevel_window.begin_resize_drag (0, 0, 0, 0, Gdk.CURRENT_TIME);
+    }
+
+    private void minimize_window_clicked () {
+        var toplevel_window = get_toplevel () as Gtk.Window;
+        if (toplevel_window == null)
+            return;
+
+        /* Turns out, we can't iconify a maximized window */
+        if (toplevel_window.is_maximized)
+            toplevel_window.unmaximize ();
+
+        toplevel_window.iconify ();
+    }
+
+    private void maximize_window_clicked () {
+        var toplevel_window = get_toplevel () as Gtk.Window;
+        if (toplevel_window == null)
+            return;
+
+        var state = toplevel_window.get_window ().get_state ();
+        if ((state & Gdk.WindowState.ICONIFIED) > 0)
+            toplevel_window.deiconify ();
+
+        toplevel_window.maximize ();
+    }
+
+    private void ontop_window_clicked () {
+        var toplevel_window = get_toplevel () as Gtk.Window;
+        if (toplevel_window == null)
+            return;
+
+        // FIXME: It will go out of sync if something else calls
+        // set_keep_above(), need to actually track it
+        keep_above = !keep_above;
+        toplevel_window.set_keep_above (keep_above);
+    }
+
+    private void close_window_clicked () {
+        var toplevel_window = get_toplevel () as Gtk.Window;
+        if (toplevel_window == null)
+            return;
+
+        toplevel_window.close ();
+    }
+
+    protected override void unrealize () {
+        if (fallback_menu != null) {
+            fallback_menu.destroy ();
+            fallback_menu = null;
+        }
+
+        base.unrealize ();
+    }
+}
diff --git a/src/ui/application-window.vala b/src/ui/application-window.vala
index 95f56d06..64587b50 100644
--- a/src/ui/application-window.vala
+++ b/src/ui/application-window.vala
@@ -1,7 +1,7 @@
 // This file is part of GNOME Games. License: GPL-3.0+.
 
 [GtkTemplate (ui = "/org/gnome/Games/ui/application-window.ui")]
-private class Games.ApplicationWindow : Gtk.ApplicationWindow {
+private class Games.ApplicationWindow : UnifiedWindow {
        private const uint WINDOW_SIZE_UPDATE_DELAY_MILLISECONDS = 500;
 
        private UiView _current_view;
diff --git a/src/ui/application.vala b/src/ui/application.vala
index 5bf626d5..b6fa9311 100644
--- a/src/ui/application.vala
+++ b/src/ui/application.vala
@@ -442,7 +442,7 @@ public class Games.Application : Gtk.Application {
 
        private void preferences () {
                if (preferences_window == null) {
-                       preferences_window = new PreferencesWindow ();
+                       preferences_window = new PreferencesWindow (this);
                        preferences_window.destroy.connect (() => {
                                preferences_window = null;
                        });
diff --git a/src/ui/preferences-subpage-gamepad.vala b/src/ui/preferences-subpage-gamepad.vala
index f24c0f93..0b3bb4f5 100644
--- a/src/ui/preferences-subpage-gamepad.vala
+++ b/src/ui/preferences-subpage-gamepad.vala
@@ -40,7 +40,11 @@ private class Games.PreferencesSubpageGamepad : Gtk.Box, PreferencesSubpage {
                        back_button.visible = (state == State.TEST);
                        cancel_button.visible = (state == State.CONFIGURE);
                        header_bar.show_close_button = (state == State.TEST);
-                       request_selection_mode = (state == State.CONFIGURE);
+                       if (state == State.CONFIGURE) {
+                               header_bar.get_style_context ().add_class ("selection-mode");
+                       } else {
+                               header_bar.get_style_context ().remove_class ("selection-mode");
+                       }
 
                        switch (value) {
                        case State.TEST:
@@ -72,15 +76,10 @@ private class Games.PreferencesSubpageGamepad : Gtk.Box, PreferencesSubpage {
                get { return _state; }
        }
 
-       [GtkChild (name = "header_bar")]
-       private Gtk.HeaderBar _header_bar;
-       public Gtk.HeaderBar header_bar {
-               get { return _header_bar; }
-       }
-
-       public bool request_selection_mode { get; set; }
        public string info_message { get; set; }
 
+       [GtkChild]
+       private Gtk.HeaderBar header_bar;
        [GtkChild]
        private Gtk.Stack gamepad_view_stack;
        [GtkChild]
diff --git a/src/ui/preferences-subpage-keyboard.vala b/src/ui/preferences-subpage-keyboard.vala
index d36ff37a..3a12150b 100644
--- a/src/ui/preferences-subpage-keyboard.vala
+++ b/src/ui/preferences-subpage-keyboard.vala
@@ -36,7 +36,11 @@ private class Games.PreferencesSubpageKeyboard : Gtk.Box, PreferencesSubpage {
                        back_button.visible = (state == State.TEST);
                        cancel_button.visible = (state == State.CONFIGURE);
                        header_bar.show_close_button = (state == State.TEST);
-                       request_selection_mode = (state == State.CONFIGURE);
+                       if (state == State.CONFIGURE) {
+                               header_bar.get_style_context ().add_class ("selection-mode");
+                       } else {
+                               header_bar.get_style_context ().remove_class ("selection-mode");
+                       }
 
                        switch (value) {
                        case State.TEST:
@@ -65,15 +69,10 @@ private class Games.PreferencesSubpageKeyboard : Gtk.Box, PreferencesSubpage {
                }
        }
 
-       [GtkChild (name = "header_bar")]
-       private Gtk.HeaderBar _header_bar;
-       public Gtk.HeaderBar header_bar {
-               get { return _header_bar; }
-       }
-
-       public bool request_selection_mode { get; set; }
        public string info_message { get; set; }
 
+       [GtkChild]
+       private Gtk.HeaderBar header_bar;
        [GtkChild]
        private Gtk.Stack gamepad_view_stack;
        [GtkChild]
diff --git a/src/ui/preferences-subpage.vala b/src/ui/preferences-subpage.vala
index 7daf840b..2dd3314f 100644
--- a/src/ui/preferences-subpage.vala
+++ b/src/ui/preferences-subpage.vala
@@ -1,6 +1,4 @@
 // This file is part of GNOME Games. License: GPL-3.0+.
 
 private interface Games.PreferencesSubpage : Gtk.Widget {
-       public abstract Gtk.HeaderBar header_bar { get; }
-       public abstract bool request_selection_mode { get; set; }
 }
diff --git a/src/ui/preferences-window.vala b/src/ui/preferences-window.vala
index 41edf6f9..6d868609 100644
--- a/src/ui/preferences-window.vala
+++ b/src/ui/preferences-window.vala
@@ -1,21 +1,17 @@
 // This file is part of GNOME Games. License: GPL-3.0+.
 
 [GtkTemplate (ui = "/org/gnome/Games/ui/preferences-window.ui")]
-private class Games.PreferencesWindow : Gtk.Window {
+private class Games.PreferencesWindow : UnifiedWindow {
        [GtkChild]
-       private Hdy.TitleBar titlebar;
-       [GtkChild]
-       private Hdy.Leaflet titlebar_leaflet;
-       [GtkChild]
-       private Gtk.Box titlebar_subpage_box;
+       private Gtk.HeaderBar left_header_bar;
        [GtkChild]
        private Gtk.HeaderBar right_header_bar;
        [GtkChild]
-       private Hdy.Deck content_deck;
+       private Hdy.Deck deck;
        [GtkChild]
-       private Hdy.Leaflet content_leaflet;
+       private Hdy.Leaflet leaflet;
        [GtkChild]
-       private Gtk.Box content_subpage_box;
+       private Gtk.Box subpage_box;
        [GtkChild]
        private PreferencesSidebar sidebar;
        [GtkChild]
@@ -35,22 +31,14 @@ private class Games.PreferencesWindow : Gtk.Window {
                                return;
 
                        if (subpage != null) {
-                               content_deck.navigate (Hdy.NavigationDirection.BACK);
-                               selection_mode_binding.unbind ();
+                               deck.navigate (Hdy.NavigationDirection.BACK);
                        }
 
                        if (value != null) {
-                               var header_bar = value.header_bar;
-
-                               content_subpage_box.add (value);
-                               titlebar_subpage_box.add (header_bar);
-
-                               selection_mode_binding = value.bind_property ("request-selection-mode",
-                                                                             titlebar, "selection-mode",
-                                                                             BindingFlags.SYNC_CREATE);
+                               subpage_box.add (value);
 
-                               content_deck.navigate (Hdy.NavigationDirection.FORWARD);
-                               content_leaflet.navigate (Hdy.NavigationDirection.FORWARD);
+                               deck.navigate (Hdy.NavigationDirection.FORWARD);
+                               leaflet.navigate (Hdy.NavigationDirection.FORWARD);
                        }
 
                        _subpage = value;
@@ -58,15 +46,18 @@ private class Games.PreferencesWindow : Gtk.Window {
        }
 
        private Binding subpage_binding;
-       private Binding selection_mode_binding;
 
        construct {
                update_ui ();
        }
 
+       public PreferencesWindow (Gtk.Application app) {
+               Object (application: app);
+       }
+
        [GtkCallback]
        private void sidebar_row_selected () {
-               content_leaflet.navigate (Hdy.NavigationDirection.FORWARD);
+               leaflet.navigate (Hdy.NavigationDirection.FORWARD);
 
                update_ui ();
        }
@@ -94,28 +85,24 @@ private class Games.PreferencesWindow : Gtk.Window {
 
        [GtkCallback]
        public void subpage_transition_finished (Object object, ParamSpec param) {
-               if (content_deck.transition_running ||
-                   content_deck.visible_child != content_leaflet)
+               if (deck.transition_running || deck.visible_child != leaflet)
                        return;
 
-               foreach (var child in content_subpage_box.get_children ())
-                       content_subpage_box.remove (child);
-
-               foreach (var child in titlebar_subpage_box.get_children ())
-                       titlebar_subpage_box.remove (child);
+               foreach (var child in subpage_box.get_children ())
+                       subpage_box.remove (child);
 
                subpage = null;
        }
 
        [GtkCallback]
        private void on_back_clicked () {
-               if (!content_leaflet.navigate (Hdy.NavigationDirection.BACK))
+               if (!leaflet.navigate (Hdy.NavigationDirection.BACK))
                        close ();
        }
 
        [GtkCallback]
        private void on_folded_changed () {
-               var folded = content_leaflet.folded;
+               var folded = leaflet.folded;
 
                update_header_group ();
                page_back_button.visible = folded;
@@ -130,11 +117,12 @@ private class Games.PreferencesWindow : Gtk.Window {
 
        [GtkCallback]
        private void update_header_group () {
-               var folded = content_leaflet.folded;
-               var visible_header_bar = titlebar_leaflet.visible_child as Gtk.HeaderBar;
-
+               var folded = leaflet.folded;
                if (folded)
-                       header_group.focus = visible_header_bar;
+                       if (leaflet.visible_child_name == "sidebar")
+                               header_group.focus = left_header_bar;
+                       else
+                               header_group.focus = right_header_bar;
                else
                        header_group.focus = null;
        }
diff --git a/src/unified-window.vala b/src/unified-window.vala
new file mode 100644
index 00000000..f59016e8
--- /dev/null
+++ b/src/unified-window.vala
@@ -0,0 +1,245 @@
+public class Games.UnifiedWindow : Gtk.ApplicationWindow {
+    private Gtk.EventBox content;
+
+    public UnifiedWindow (Gtk.Application app) {
+        Object (application: app);
+    }
+
+    construct {
+        get_style_context ().add_class ("unified");
+
+        // Trick the window into being CSD
+        set_titlebar (new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0));
+
+        ensure_content ();
+    }
+
+    private void ensure_content () {
+        if (content != null)
+            return;
+
+        content = new Gtk.EventBox ();
+        content.expand = true;
+        content.show ();
+        base.add (content);
+    }
+
+    public override void add (Gtk.Widget widget) {
+        if (widget is Gtk.Popover) {
+            base.add (widget);
+            return;
+        }
+
+        ensure_content ();
+
+        content.add (widget);
+    }
+
+    public override void remove (Gtk.Widget widget) {
+        if (widget == content || widget is Gtk.Popover) {
+            base.remove (widget);
+            return;
+        }
+
+        content.remove (widget);
+    }
+
+    public override void forall_internal (bool include_internal, Gtk.Callback callback) {
+        if (include_internal)
+            base.forall_internal (include_internal, callback);
+        else if (content != null)
+            content.forall_internal (include_internal, callback);
+    }
+
+    private Gtk.StyleContext get_child_context (string name) {
+        var parent = get_style_context ();
+
+        var path = parent.get_path ().copy ();
+        var pos = path.append_type (typeof (Gtk.Widget));
+        path.iter_set_object_name (pos, name);
+
+        var context = new Gtk.StyleContext ();
+        context.set_path (path);
+        context.set_screen (parent.get_screen ());
+        context.set_state (parent.get_state ());
+        context.set_scale (parent.get_scale ());
+        context.set_frame_clock (parent.get_frame_clock ());
+
+        return context;
+    }
+
+    protected override bool draw (Cairo.Context cr) {
+        var mask_corners = (decorated && !is_fullscreen () && !is_maximized);
+
+        if (Gtk.cairo_should_draw_window (cr, get_window ())) {
+            var width = get_allocated_width ();
+            var height = get_allocated_height ();
+
+            int x = 0;
+            int y = 0;
+            int w = width;
+            int h = height;
+
+            var context = get_child_context ("decoration");
+
+            if (mask_corners) {
+                var state = context.get_state ();
+                var border = context.get_border (state);
+                var padding = context.get_padding (state);
+                border = sum_borders (border, padding);
+
+                var shadow = get_shadow_width (context);
+
+                x = shadow.left - border.left;
+                y = shadow.top - border.top;
+                w = width - (shadow.left + shadow.right - border.left - border.right);
+                h = height - (shadow.top + shadow.bottom - border.top - border.bottom);
+            }
+
+            // GtkWindow adds this when it can't draw proper decorations, e.g. on a
+            // non-composited WM on X11. This is documented, so we can rely on this
+            // instead of copying the (pretty extensive) check.
+            if (get_style_context ().has_class ("solid-csd")) {
+                context.render_background (cr, 0, 0, width, height);
+                context.render_frame (cr, 0, 0, width, height);
+            } else {
+                context.render_background (cr, x, y, w, h);
+                context.render_frame (cr, x, y, w, h);
+            }
+
+            cr.save ();
+
+            if (mask_corners)
+                cr.push_group ();
+
+            if (!get_app_paintable ()) {
+                context = get_style_context ();
+                context.render_background (cr, x, y, w, h);
+                context.render_frame (cr, x, y, w, h);
+            }
+
+            propagate_draw (content, cr);
+
+            context = get_child_context ("decoration-overlay");
+            context.render_background (cr, x, y, w, h);
+            context.render_frame (cr, x, y, w, h);
+
+            if (mask_corners) {
+                cr.pop_group_to_source ();
+                cr.mask_surface (get_mask (w, h), x, y);
+            }
+
+            cr.restore ();
+        }
+
+        forall (child => {
+            if (child == content || child == get_titlebar ())
+                return;
+
+            if (!child.visible || !child.get_child_visible ())
+                return;
+
+            var window = child.get_window ();
+            if (child.get_has_window ())
+                window = window.get_parent ();
+
+            if (!Gtk.cairo_should_draw_window (cr, window))
+                return;
+
+            propagate_draw (child, cr);
+        });
+
+        return Gdk.EVENT_PROPAGATE;
+    }
+
+    private bool is_fullscreen () {
+        return (get_window ().get_state () & Gdk.WindowState.FULLSCREEN) > 0;
+    }
+
+    private Gtk.Border sum_borders (Gtk.Border a, Gtk.Border b) {
+        return {
+            a.left + b.left,
+            a.right + b.right,
+            a.top + b.top,
+            a.bottom + b.bottom,
+        };
+    }
+
+    private Gtk.Border max_borders (Gtk.Border a, Gtk.Border b) {
+        return {
+            int16.max (a.left, b.left),
+            int16.max (a.right, b.right),
+            int16.max (a.top, b.top),
+            int16.max (a.bottom, b.bottom),
+        };
+    }
+
+    private Gtk.Border get_shadow_width (Gtk.StyleContext context) {
+        if (!decorated)
+            return {};
+
+        if (is_maximized || is_fullscreen ())
+            return {};
+
+        if (!is_toplevel ())
+            return {};
+
+        var state = context.get_state ();
+
+        var border = context.get_border (state);
+        var padding = context.get_padding (state);
+        var margin = context.get_margin (state);
+
+        Gtk.Allocation alloc = {};
+        Gtk.Allocation content_alloc = {};
+
+        get_allocation (out alloc);
+        content.get_allocation (out content_alloc);
+
+        Gtk.Border shadow = {
+            (int16) (content_alloc.x - alloc.x),
+            (int16) (alloc.width - content_alloc.width - content_alloc.x),
+            (int16) (content_alloc.y - alloc.y),
+            (int16) (alloc.height - content_alloc.height - content_alloc.y),
+        };
+
+        border = sum_borders (padding, border);
+        shadow = max_borders (shadow, margin);
+
+        return sum_borders (border, shadow);
+    }
+
+    // TODO: this is very naive, pretty sure it can be done a lot more efficiently
+    private Cairo.Surface get_mask (int w, int h) {
+        var style = get_style_context ();
+        var state = style.get_state ();
+
+        var mask = new Cairo.ImageSurface (Cairo.Format.A8, w * scale_factor, h * scale_factor);
+
+        double border_radius = (int) style.get_property (Gtk.STYLE_PROPERTY_BORDER_RADIUS, state);
+        border_radius = border_radius.clamp (0, double.max (w / 2, h / 2));
+
+        var cr = new Cairo.Context (mask);
+        cr.set_source_rgb (0, 0, 0);
+        rounded_rectangle (cr, 0, 0, w, h, border_radius);
+        cr.fill ();
+
+        return mask;
+    }
+
+    // FIXME: Need to support different radii for different corners.
+    // There doesn't seem to be a way to get that from GTK though.
+    private void rounded_rectangle (Cairo.Context cr, double x, double y, double width, double height, 
double radius) {
+        const double ARC_0 = 0;
+        const double ARC_1 = Math.PI * 0.5;
+        const double ARC_2 = Math.PI;
+        const double ARC_3 = Math.PI * 1.5;
+
+        cr.new_sub_path ();
+        cr.arc (x + width - radius, y + radius,          radius, ARC_3, ARC_0);
+        cr.arc (x + width - radius, y + height - radius, radius, ARC_0, ARC_1);
+        cr.arc (x + radius,         y + height - radius, radius, ARC_1, ARC_2);
+        cr.arc (x + radius,         y + radius,          radius, ARC_2, ARC_3);
+        cr.close_path ();
+    }
+}


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]