[fractal/fractal-next] sidebar: Add Explore entry



commit bd13ae4ef233c6065972818725ba0fb6d95d401b
Author: Julian Sparber <julian sparber net>
Date:   Thu Jun 3 19:22:59 2021 +0200

    sidebar: Add Explore entry

 .../icons/scalable/status/explore-symbolic.svg     | 151 ++++++++++++++++++++
 data/resources/resources.gresource.xml             |   2 +
 data/resources/style.css                           |   5 +
 data/resources/ui/session.ui                       |   2 +
 data/resources/ui/sidebar-entry-row.ui             |  36 +++++
 po/POTFILES.in                                     |   2 +
 src/session/content/content.rs                     |  61 +++++++--
 src/session/content/content_type.rs                |  27 ++++
 src/session/content/mod.rs                         |   2 +
 src/session/mod.rs                                 |  43 +++++-
 src/session/sidebar/entry.rs                       | 109 +++++++++++++++
 src/session/sidebar/entry_row.rs                   | 101 ++++++++++++++
 src/session/sidebar/item_list.rs                   |  24 ++--
 src/session/sidebar/mod.rs                         |   4 +
 src/session/sidebar/row.rs                         |  17 ++-
 src/session/sidebar/selection.rs                   | 152 +++++++++++++++------
 src/session/sidebar/sidebar.rs                     |  44 +++++-
 17 files changed, 714 insertions(+), 68 deletions(-)
---
diff --git a/data/resources/icons/scalable/status/explore-symbolic.svg 
b/data/resources/icons/scalable/status/explore-symbolic.svg
new file mode 100644
index 00000000..3dac6a5e
--- /dev/null
+++ b/data/resources/icons/scalable/status/explore-symbolic.svg
@@ -0,0 +1,151 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg"; 
xmlns:xlink="http://www.w3.org/1999/xlink";>
+    <filter id="a" height="100%" width="100%" x="0%" y="0%">
+        <feColorMatrix in="SourceGraphic" type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/>
+    </filter>
+    <mask id="b">
+        <g filter="url(#a)">
+            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.3"/>
+        </g>
+    </mask>
+    <clipPath id="c">
+        <path d="m 0 0 h 1024 v 800 h -1024 z"/>
+    </clipPath>
+    <mask id="d">
+        <g filter="url(#a)">
+            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
+        </g>
+    </mask>
+    <clipPath id="e">
+        <path d="m 0 0 h 1024 v 800 h -1024 z"/>
+    </clipPath>
+    <mask id="f">
+        <g filter="url(#a)">
+            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
+        </g>
+    </mask>
+    <clipPath id="g">
+        <path d="m 0 0 h 1024 v 800 h -1024 z"/>
+    </clipPath>
+    <mask id="h">
+        <g filter="url(#a)">
+            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
+        </g>
+    </mask>
+    <clipPath id="i">
+        <path d="m 0 0 h 1024 v 800 h -1024 z"/>
+    </clipPath>
+    <mask id="j">
+        <g filter="url(#a)">
+            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
+        </g>
+    </mask>
+    <clipPath id="k">
+        <path d="m 0 0 h 1024 v 800 h -1024 z"/>
+    </clipPath>
+    <mask id="l">
+        <g filter="url(#a)">
+            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
+        </g>
+    </mask>
+    <clipPath id="m">
+        <path d="m 0 0 h 1024 v 800 h -1024 z"/>
+    </clipPath>
+    <mask id="n">
+        <g filter="url(#a)">
+            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
+        </g>
+    </mask>
+    <clipPath id="o">
+        <path d="m 0 0 h 1024 v 800 h -1024 z"/>
+    </clipPath>
+    <mask id="p">
+        <g filter="url(#a)">
+            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.3"/>
+        </g>
+    </mask>
+    <clipPath id="q">
+        <path d="m 0 0 h 1024 v 800 h -1024 z"/>
+    </clipPath>
+    <mask id="r">
+        <g filter="url(#a)">
+            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.5"/>
+        </g>
+    </mask>
+    <clipPath id="s">
+        <path d="m 0 0 h 1024 v 800 h -1024 z"/>
+    </clipPath>
+    <mask id="t">
+        <g filter="url(#a)">
+            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.4"/>
+        </g>
+    </mask>
+    <clipPath id="u">
+        <path d="m 0 0 h 1024 v 800 h -1024 z"/>
+    </clipPath>
+    <mask id="v">
+        <g filter="url(#a)">
+            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.4"/>
+        </g>
+    </mask>
+    <clipPath id="w">
+        <path d="m 0 0 h 1024 v 800 h -1024 z"/>
+    </clipPath>
+    <mask id="x">
+        <g filter="url(#a)">
+            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.5"/>
+        </g>
+    </mask>
+    <clipPath id="y">
+        <path d="m 0 0 h 1024 v 800 h -1024 z"/>
+    </clipPath>
+    <mask id="z">
+        <g filter="url(#a)">
+            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.5"/>
+        </g>
+    </mask>
+    <clipPath id="A">
+        <path d="m 0 0 h 1024 v 800 h -1024 z"/>
+    </clipPath>
+    <g clip-path="url(#c)" mask="url(#b)" transform="matrix(1 0 0 1 -600 -80)">
+        <path d="m 562.460938 212.058594 h 10.449218 c -1.183594 0.492187 -1.296875 2.460937 0 3 h 
-10.449218 z m 0 0" fill="#2e3436"/>
+    </g>
+    <path d="m 8 1 c -0.207031 0 -0.390625 0.125 -0.46875 0.320312 l -1.726562 4.488282 l -4.484376 1.722656 
c -0.425781 0.167969 -0.425781 0.769531 0 0.9375 l 4.484376 1.722656 l 1.726562 4.488282 c 0.167969 0.425781 
0.769531 0.425781 0.9375 0 l 1.726562 -4.488282 l 4.484376 -1.722656 c 0.425781 -0.167969 0.425781 -0.769531 
0 -0.9375 l -4.484376 -1.722656 l -1.726562 -4.488282 c -0.074219 -0.191406 -0.261719 -0.320312 -0.46875 
-0.320312 z m 0 6 c 0.550781 0 1 0.449219 1 1 s -0.449219 1 -1 1 s -1 -0.449219 -1 -1 s 0.449219 -1 1 -1 z m 
0 0" fill="#2e3436" fill-rule="evenodd"/>
+    <path d="m 3.480469 3 c -0.363281 0.011719 -0.589844 0.398438 -0.429688 0.722656 l 0.871094 1.738282 l 
1.109375 -0.429688 l 0.429688 -1.109375 l -1.738282 -0.871094 c -0.074218 -0.035156 -0.15625 -0.054687 
-0.242187 -0.050781 z m 9.039062 0 c -0.085937 -0.003906 -0.167969 0.015625 -0.242187 0.050781 l -1.738282 
0.871094 l 0.429688 1.109375 l 1.109375 0.429688 l 0.871094 -1.738282 c 0.160156 -0.324218 -0.066407 
-0.710937 -0.429688 -0.722656 z m -8.597656 7.539062 l -0.871094 1.738282 c -0.210937 0.429687 0.242188 
0.882812 0.671875 0.671875 l 1.738282 -0.871094 l -0.429688 -1.109375 z m 8.15625 0 l -1.109375 0.429688 l 
-0.429688 1.109375 l 1.738282 0.871094 c 0.429687 0.210937 0.882812 -0.242188 0.671875 -0.671875 z m 0 0" 
fill="#2e3436" fill-rule="evenodd"/>
+    <g clip-path="url(#e)" mask="url(#d)" transform="matrix(1 0 0 1 -600 -80)">
+        <path d="m 16 632 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
+    </g>
+    <g clip-path="url(#g)" mask="url(#f)" transform="matrix(1 0 0 1 -600 -80)">
+        <path d="m 17 631 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
+    </g>
+    <g clip-path="url(#i)" mask="url(#h)" transform="matrix(1 0 0 1 -600 -80)">
+        <path d="m 18 634 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
+    </g>
+    <g clip-path="url(#k)" mask="url(#j)" transform="matrix(1 0 0 1 -600 -80)">
+        <path d="m 16 634 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
+    </g>
+    <g clip-path="url(#m)" mask="url(#l)" transform="matrix(1 0 0 1 -600 -80)">
+        <path d="m 17 635 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
+    </g>
+    <g clip-path="url(#o)" mask="url(#n)" transform="matrix(1 0 0 1 -600 -80)">
+        <path d="m 19 635 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
+    </g>
+    <g clip-path="url(#q)" mask="url(#p)" transform="matrix(1 0 0 1 -600 -80)">
+        <path d="m 136 660 v 7 h 7 v -7 z m 0 0" fill="#2e3436"/>
+    </g>
+    <g clip-path="url(#s)" mask="url(#r)" transform="matrix(1 0 0 1 -600 -80)">
+        <path d="m 199 642 h 3 v 12 h -3 z m 0 0" fill="#2e3436"/>
+    </g>
+    <g clip-path="url(#u)" mask="url(#t)" transform="matrix(1 0 0 1 -600 -80)">
+        <path d="m 209.5 144.160156 c 0.277344 0 0.5 0.222656 0.5 0.5 v 1 c 0 0.277344 -0.222656 0.5 -0.5 
0.5 s -0.5 -0.222656 -0.5 -0.5 v -1 c 0 -0.277344 0.222656 -0.5 0.5 -0.5 z m 0 0" fill="#2e3436"/>
+    </g>
+    <g clip-path="url(#w)" mask="url(#v)" transform="matrix(1 0 0 1 -600 -80)">
+        <path d="m 206.5 144.160156 c 0.277344 0 0.5 0.222656 0.5 0.5 v 1 c 0 0.277344 -0.222656 0.5 -0.5 
0.5 s -0.5 -0.222656 -0.5 -0.5 v -1 c 0 -0.277344 0.222656 -0.5 0.5 -0.5 z m 0 0" fill="#2e3436"/>
+    </g>
+    <g clip-path="url(#y)" mask="url(#x)" transform="matrix(1 0 0 1 -600 -80)">
+        <path d="m 229.5 143.160156 c -0.546875 0 -1 0.457032 -1 1 c 0 0.546875 0.453125 1 1 1 s 1 -0.453125 
1 -1 c 0 -0.542968 -0.453125 -1 -1 -1 z m 0 0" fill="#2e3436"/>
+    </g>
+    <g clip-path="url(#A)" mask="url(#z)" transform="matrix(1 0 0 1 -600 -80)">
+        <path d="m 226.453125 143.160156 c -0.519531 0 -0.953125 0.433594 -0.953125 0.953125 v 0.09375 c 0 
0.519531 0.433594 0.953125 0.953125 0.953125 h 0.09375 c 0.519531 0 0.953125 -0.433594 0.953125 -0.953125 v 
-0.09375 c 0 -0.519531 -0.433594 -0.953125 -0.953125 -0.953125 z m 0 0" fill="#2e3436"/>
+    </g>
+</svg>
diff --git a/data/resources/resources.gresource.xml b/data/resources/resources.gresource.xml
index 4e2683c3..ebcba03e 100644
--- a/data/resources/resources.gresource.xml
+++ b/data/resources/resources.gresource.xml
@@ -17,6 +17,7 @@
     <file compressed="true" preprocess="xml-stripblanks" alias="sidebar.ui">ui/sidebar.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" alias="sidebar-item.ui">ui/sidebar-item.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" 
alias="sidebar-category-row.ui">ui/sidebar-category-row.ui</file>
+    <file compressed="true" preprocess="xml-stripblanks" 
alias="sidebar-entry-row.ui">ui/sidebar-entry-row.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" 
alias="sidebar-room-row.ui">ui/sidebar-room-row.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" alias="window.ui">ui/window.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" 
alias="context-menu-bin.ui">ui/context-menu-bin.ui</file>
@@ -27,6 +28,7 @@
     <file compressed="true">style.css</file>
     <file preprocess="xml-stripblanks">icons/scalable/actions/send-symbolic.svg</file>
     <file preprocess="xml-stripblanks">icons/scalable/status/welcome.svg</file>
+    <file preprocess="xml-stripblanks">icons/scalable/status/explore-symbolic.svg</file>
   </gresource>
 </gresources>
 
diff --git a/data/resources/style.css b/data/resources/style.css
index 91f3b54d..ee1a5c59 100644
--- a/data/resources/style.css
+++ b/data/resources/style.css
@@ -46,6 +46,11 @@ headerbar.flat {
   font-weight: bold;
 }
 
+.sidebar .entry {
+  margin-top: 4px;
+  font-weight: bold;
+}
+
 .sidebar .category image.arrow {
   transition: 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94);
 }
diff --git a/data/resources/ui/session.ui b/data/resources/ui/session.ui
index 590cf2cc..363738c9 100644
--- a/data/resources/ui/session.ui
+++ b/data/resources/ui/session.ui
@@ -51,12 +51,14 @@
                     <property name="compact" bind-source="content" bind-property="folded" 
bind-flags="sync-create"/>
                     <property name="room-list" bind-source="Session" bind-property="room-list" 
bind-flags="sync-create"/>
                     <property name="selected-room" bind-source="Session" bind-property="selected-room" 
bind-flags="sync-create | bidirectional"/>
+                    <property name="selected-type" bind-source="Session" 
bind-property="selected-content-type" bind-flags="sync-create | bidirectional"/>
                   </object>
                 </child>
                 <child>
                   <object class="Content">
                     <property name="compact" bind-source="content" bind-property="folded" 
bind-flags="sync-create"/>
                     <property name="room" bind-source="Session" bind-property="selected-room" 
bind-flags="sync-create | bidirectional"/>
+                    <property name="content-type" bind-source="Session" 
bind-property="selected-content-type" bind-flags="sync-create | bidirectional"/>
                     <property name="error-list">error_list</property>
                   </object>
                 </child>
diff --git a/data/resources/ui/sidebar-entry-row.ui b/data/resources/ui/sidebar-entry-row.ui
new file mode 100644
index 00000000..4082eea1
--- /dev/null
+++ b/data/resources/ui/sidebar-entry-row.ui
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="SidebarEntryRow" parent="AdwBin">
+    <style>
+      <class name="entry-row"/>
+    </style>
+    <child>
+      <object class="GtkBox">
+        <property name="spacing">12</property>
+        <child>
+          <object class="GtkImage">
+            <property name="icon-name">explore2-symbolic</property>
+            <binding name="icon-name">
+              <lookup name="icon-name" type="Entry">
+                <lookup name="entry">SidebarEntryRow</lookup>
+              </lookup>
+            </binding>
+          </object>
+        </child>
+        <child>
+          <object class="GtkLabel">
+            <property name="halign">start</property>
+            <property name="hexpand">True</property>
+            <property name="ellipsize">end</property>
+            <binding name="label">
+              <lookup name="display-name" type="Entry">
+                <lookup name="entry">SidebarEntryRow</lookup>
+              </lookup>
+            </binding>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
+
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 28cd433e..41d6c374 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -22,6 +22,7 @@ data/resources/ui/in-app-notification.ui
 data/resources/ui/session.ui
 data/resources/ui/shortcuts.ui
 data/resources/ui/sidebar-category-row.ui
+data/resources/ui/sidebar-entry-row.ui
 data/resources/ui/sidebar-item.ui
 data/resources/ui/sidebar-room-row.ui
 data/resources/ui/sidebar.ui
@@ -64,6 +65,7 @@ src/session/room/mod.rs
 src/session/room/room.rs
 src/session/room/timeline.rs
 src/session/sidebar/category_row.rs
+src/session/sidebar/entry.rs
 src/session/sidebar/mod.rs
 src/session/sidebar/room_row.rs
 src/session/sidebar/row.rs
diff --git a/src/session/content/content.rs b/src/session/content/content.rs
index d7dd8166..ac16a977 100644
--- a/src/session/content/content.rs
+++ b/src/session/content/content.rs
@@ -1,4 +1,5 @@
 use crate::session::{
+    content::ContentType,
     content::Invite,
     content::RoomHistory,
     room::{Room, RoomType},
@@ -16,6 +17,7 @@ mod imp {
     pub struct Content {
         pub compact: Cell<bool>,
         pub room: RefCell<Option<Room>>,
+        pub content_type: Cell<ContentType>,
         pub error_list: RefCell<Option<gio::ListStore>>,
         pub category_handler: RefCell<Option<SignalHandlerId>>,
         #[template_child]
@@ -39,7 +41,7 @@ mod imp {
             klass.set_accessible_role(gtk::AccessibleRole::Group);
 
             klass.install_action("content.go-back", None, move |widget, _, _| {
-                widget.set_room(None);
+                widget.set_content_type(ContentType::None);
             });
         }
 
@@ -74,6 +76,14 @@ mod imp {
                         gio::ListStore::static_type(),
                         glib::ParamFlags::READWRITE,
                     ),
+                    glib::ParamSpec::new_enum(
+                        "content-type",
+                        "Content Type",
+                        "The type of content currently displayed",
+                        ContentType::static_type(),
+                        ContentType::default() as i32,
+                        glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
+                    ),
                 ]
             });
 
@@ -99,6 +109,7 @@ mod imp {
                 "error-list" => {
                     self.error_list.replace(value.get().unwrap());
                 }
+                "content-type" => obj.set_content_type(value.get().unwrap()),
                 _ => unimplemented!(),
             }
         }
@@ -108,6 +119,7 @@ mod imp {
                 "compact" => self.compact.get().to_value(),
                 "room" => obj.room().to_value(),
                 "error-list" => self.error_list.borrow().to_value(),
+                "content-type" => obj.content_type().to_value(),
                 _ => unimplemented!(),
             }
         }
@@ -127,6 +139,24 @@ impl Content {
         glib::Object::new(&[]).expect("Failed to create Content")
     }
 
+    pub fn content_type(&self) -> ContentType {
+        let priv_ = imp::Content::from_instance(self);
+        priv_.content_type.get()
+    }
+
+    pub fn set_content_type(&self, content_type: ContentType) {
+        let priv_ = imp::Content::from_instance(self);
+
+        if self.content_type() == content_type {
+            return;
+        }
+
+        priv_.content_type.set(content_type);
+        self.set_visible_child();
+
+        self.notify("content-type");
+    }
+
     pub fn set_room(&self, room: Option<Room>) {
         let priv_ = imp::Content::from_instance(self);
 
@@ -143,17 +173,16 @@ impl Content {
         if let Some(ref room) = room {
             let handler_id = room.connect_notify_local(
                 Some("category"),
-                clone!(@weak self as obj => move |room, _| {
-                        obj.set_visible_child(room);
+                clone!(@weak self as obj => move |_, _| {
+                        obj.set_visible_child();
                 }),
             );
 
-            self.set_visible_child(&room);
             priv_.category_handler.replace(Some(handler_id));
         }
 
         priv_.room.replace(room);
-
+        self.set_visible_child();
         self.notify("room");
     }
 
@@ -162,13 +191,25 @@ impl Content {
         priv_.room.borrow().clone()
     }
 
-    fn set_visible_child(&self, room: &Room) {
+    fn set_visible_child(&self) {
         let priv_ = imp::Content::from_instance(self);
 
-        if room.category() == RoomType::Invited {
-            priv_.stack.set_visible_child(&*priv_.invite);
-        } else {
-            priv_.stack.set_visible_child(&*priv_.room_history);
+        match self.content_type() {
+            ContentType::None => {
+                //TODO: display an empty state
+            }
+            ContentType::Room => {
+                if let Some(room) = &*priv_.room.borrow() {
+                    if room.category() == RoomType::Invited {
+                        priv_.stack.set_visible_child(&*priv_.invite);
+                    } else {
+                        priv_.stack.set_visible_child(&*priv_.room_history);
+                    }
+                }
+            }
+            ContentType::Explore => {
+                todo!("Display explore");
+            }
         }
     }
 }
diff --git a/src/session/content/content_type.rs b/src/session/content/content_type.rs
new file mode 100644
index 00000000..50eff2a5
--- /dev/null
+++ b/src/session/content/content_type.rs
@@ -0,0 +1,27 @@
+use gettextrs::gettext;
+use gtk::glib;
+
+#[derive(Debug, Hash, Eq, PartialEq, Clone, Copy, glib::GEnum)]
+#[repr(u32)]
+#[genum(type_name = "ContentType")]
+pub enum ContentType {
+    None = 0,
+    Explore = 1,
+    Room = 2,
+}
+
+impl Default for ContentType {
+    fn default() -> Self {
+        ContentType::None
+    }
+}
+
+impl ToString for ContentType {
+    fn to_string(&self) -> String {
+        match self {
+            ContentType::None => gettext("No selection"),
+            ContentType::Explore => gettext("Explore"),
+            ContentType::Room => gettext("Room"),
+        }
+    }
+}
diff --git a/src/session/content/mod.rs b/src/session/content/mod.rs
index 3109a2ed..0de3f250 100644
--- a/src/session/content/mod.rs
+++ b/src/session/content/mod.rs
@@ -1,4 +1,5 @@
 mod content;
+mod content_type;
 mod divider_row;
 mod invite;
 mod item_row;
@@ -8,6 +9,7 @@ mod room_history;
 mod state_row;
 
 pub use self::content::Content;
+pub use self::content_type::ContentType;
 use self::divider_row::DividerRow;
 use self::invite::Invite;
 use self::item_row::ItemRow;
diff --git a/src/session/mod.rs b/src/session/mod.rs
index 470a5006..07c13f56 100644
--- a/src/session/mod.rs
+++ b/src/session/mod.rs
@@ -20,6 +20,7 @@ use crate::utils::do_async;
 use crate::Error;
 use crate::RUNTIME;
 
+use crate::session::content::ContentType;
 use adw;
 use adw::subclass::prelude::BinImpl;
 use gtk::subclass::prelude::*;
@@ -41,7 +42,7 @@ mod imp {
     use super::*;
     use glib::subclass::{InitializingObject, Signal};
     use once_cell::sync::{Lazy, OnceCell};
-    use std::cell::RefCell;
+    use std::cell::{Cell, RefCell};
 
     #[derive(Debug, Default, CompositeTemplate)]
     #[template(resource = "/org/gnome/FractalNext/session.ui")]
@@ -58,6 +59,7 @@ mod imp {
         pub room_list: OnceCell<RoomList>,
         pub user: OnceCell<User>,
         pub selected_room: RefCell<Option<Room>>,
+        pub selected_content_type: Cell<ContentType>,
         pub is_ready: OnceCell<bool>,
     }
 
@@ -98,6 +100,14 @@ mod imp {
                         Room::static_type(),
                         glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
                     ),
+                    glib::ParamSpec::new_enum(
+                        "selected-content-type",
+                        "Selected Content Type",
+                        "The current content type selected",
+                        ContentType::static_type(),
+                        ContentType::default() as i32,
+                        glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
+                    ),
                     glib::ParamSpec::new_object(
                         "user",
                         "User",
@@ -123,6 +133,7 @@ mod imp {
                     let selected_room = value.get().unwrap();
                     obj.set_selected_room(selected_room);
                 }
+                "selected-content-type" => obj.set_selected_content_type(value.get().unwrap()),
                 _ => unimplemented!(),
             }
         }
@@ -132,6 +143,7 @@ mod imp {
                 "room-list" => obj.room_list().to_value(),
                 "selected-room" => obj.selected_room().to_value(),
                 "user" => obj.user().to_value(),
+                "selected-content-type" => obj.selected_content_type().to_value(),
                 _ => unimplemented!(),
             }
         }
@@ -157,6 +169,29 @@ impl Session {
         glib::Object::new(&[]).expect("Failed to create Session")
     }
 
+    pub fn selected_content_type(&self) -> ContentType {
+        let priv_ = imp::Session::from_instance(self);
+        priv_.selected_content_type.get()
+    }
+
+    pub fn set_selected_content_type(&self, selected_type: ContentType) {
+        let priv_ = imp::Session::from_instance(self);
+
+        if self.selected_content_type() == selected_type {
+            return;
+        }
+
+        if selected_type == ContentType::None {
+            priv_.content.navigate(adw::NavigationDirection::Back);
+        } else {
+            priv_.content.navigate(adw::NavigationDirection::Forward);
+        }
+
+        priv_.selected_content_type.set(selected_type);
+
+        self.notify("selected-content-type");
+    }
+
     pub fn selected_room(&self) -> Option<Room> {
         let priv_ = imp::Session::from_instance(self);
         priv_.selected_room.borrow().clone()
@@ -169,12 +204,6 @@ impl Session {
             return;
         }
 
-        if selected_room.is_some() {
-            priv_.content.navigate(adw::NavigationDirection::Forward);
-        } else {
-            priv_.content.navigate(adw::NavigationDirection::Back);
-        }
-
         priv_.selected_room.replace(selected_room);
 
         self.notify("selected-room");
diff --git a/src/session/sidebar/entry.rs b/src/session/sidebar/entry.rs
new file mode 100644
index 00000000..0aea94ad
--- /dev/null
+++ b/src/session/sidebar/entry.rs
@@ -0,0 +1,109 @@
+use gtk::{glib, prelude::*, subclass::prelude::*};
+
+use crate::session::content::ContentType;
+
+mod imp {
+    use std::cell::{Cell, RefCell};
+
+    use super::*;
+
+    #[derive(Debug, Default)]
+    pub struct Entry {
+        pub type_: Cell<ContentType>,
+        pub display_name: RefCell<Option<String>>,
+        pub icon_name: RefCell<Option<String>>,
+    }
+
+    #[glib::object_subclass]
+    impl ObjectSubclass for Entry {
+        const NAME: &'static str = "Entry";
+        type Type = super::Entry;
+        type ParentType = glib::Object;
+    }
+
+    impl ObjectImpl for Entry {
+        fn properties() -> &'static [glib::ParamSpec] {
+            use once_cell::sync::Lazy;
+            static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
+                vec![
+                    glib::ParamSpec::new_enum(
+                        "type",
+                        "Type",
+                        "The type of this category",
+                        ContentType::static_type(),
+                        ContentType::default() as i32,
+                        glib::ParamFlags::READWRITE | glib::ParamFlags::CONSTRUCT_ONLY,
+                    ),
+                    glib::ParamSpec::new_string(
+                        "display-name",
+                        "Display Name",
+                        "The display name of this Entry",
+                        None,
+                        glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
+                    ),
+                    glib::ParamSpec::new_string(
+                        "icon-name",
+                        "Icon Name",
+                        "The icon name used for this Entry",
+                        None,
+                        glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
+                    ),
+                ]
+            });
+
+            PROPERTIES.as_ref()
+        }
+
+        fn set_property(
+            &self,
+            _obj: &Self::Type,
+            _id: usize,
+            value: &glib::Value,
+            pspec: &glib::ParamSpec,
+        ) {
+            match pspec.name() {
+                "type" => {
+                    self.type_.set(value.get().unwrap());
+                }
+                "display-name" => {
+                    let _ = self.display_name.replace(value.get().unwrap());
+                }
+                "icon-name" => {
+                    let _ = self.icon_name.replace(value.get().unwrap());
+                }
+                _ => unimplemented!(),
+            }
+        }
+
+        fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
+            match pspec.name() {
+                "type" => obj.type_().to_value(),
+                "display-name" => obj.type_().to_string().to_value(),
+                "icon-name" => obj.icon_name().to_value(),
+                _ => unimplemented!(),
+            }
+        }
+    }
+}
+
+glib::wrapper! {
+    pub struct Entry(ObjectSubclass<imp::Entry>);
+}
+
+impl Entry {
+    pub fn new(type_: ContentType) -> Self {
+        glib::Object::new(&[("type", &type_)]).expect("Failed to create Entry")
+    }
+
+    pub fn type_(&self) -> ContentType {
+        let priv_ = imp::Entry::from_instance(self);
+        priv_.type_.get()
+    }
+
+    pub fn icon_name(&self) -> Option<&str> {
+        match self.type_() {
+            ContentType::Explore => Some("explore-symbolic"),
+            _ => None,
+        }
+    }
+}
diff --git a/src/session/sidebar/entry_row.rs b/src/session/sidebar/entry_row.rs
new file mode 100644
index 00000000..bf7faa0c
--- /dev/null
+++ b/src/session/sidebar/entry_row.rs
@@ -0,0 +1,101 @@
+use adw;
+use adw::subclass::prelude::BinImpl;
+use gtk::subclass::prelude::*;
+use gtk::{self, prelude::*};
+use gtk::{glib, CompositeTemplate};
+
+use crate::session::sidebar::Entry;
+
+mod imp {
+    use super::*;
+    use glib::subclass::InitializingObject;
+    use std::cell::RefCell;
+
+    #[derive(Debug, Default, CompositeTemplate)]
+    #[template(resource = "/org/gnome/FractalNext/sidebar-entry-row.ui")]
+    pub struct EntryRow {
+        pub entry: RefCell<Option<Entry>>,
+    }
+
+    #[glib::object_subclass]
+    impl ObjectSubclass for EntryRow {
+        const NAME: &'static str = "SidebarEntryRow";
+        type Type = super::EntryRow;
+        type ParentType = adw::Bin;
+
+        fn class_init(klass: &mut Self::Class) {
+            Self::bind_template(klass);
+        }
+
+        fn instance_init(obj: &InitializingObject<Self>) {
+            obj.init_template();
+        }
+    }
+
+    impl ObjectImpl for EntryRow {
+        fn properties() -> &'static [glib::ParamSpec] {
+            use once_cell::sync::Lazy;
+            static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
+                vec![glib::ParamSpec::new_object(
+                    "entry",
+                    "Entry",
+                    "The entry of this row",
+                    Entry::static_type(),
+                    glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
+                )]
+            });
+
+            PROPERTIES.as_ref()
+        }
+
+        fn set_property(
+            &self,
+            obj: &Self::Type,
+            _id: usize,
+            value: &glib::Value,
+            pspec: &glib::ParamSpec,
+        ) {
+            match pspec.name() {
+                "entry" => obj.set_entry(value.get().unwrap()),
+                _ => unimplemented!(),
+            }
+        }
+
+        fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
+            match pspec.name() {
+                "entry" => obj.entry().to_value(),
+                _ => unimplemented!(),
+            }
+        }
+    }
+
+    impl WidgetImpl for EntryRow {}
+    impl BinImpl for EntryRow {}
+}
+
+glib::wrapper! {
+    pub struct EntryRow(ObjectSubclass<imp::EntryRow>)
+        @extends gtk::Widget, adw::Bin, @implements gtk::Accessible;
+}
+
+impl EntryRow {
+    pub fn new() -> Self {
+        glib::Object::new(&[]).expect("Failed to create EntryRow")
+    }
+
+    pub fn entry(&self) -> Option<Entry> {
+        let priv_ = imp::EntryRow::from_instance(&self);
+        priv_.entry.borrow().clone()
+    }
+
+    pub fn set_entry(&self, entry: Option<Entry>) {
+        let priv_ = imp::EntryRow::from_instance(&self);
+
+        if self.entry() == entry {
+            return;
+        }
+
+        priv_.entry.replace(entry);
+        self.notify("entry");
+    }
+}
diff --git a/src/session/sidebar/item_list.rs b/src/session/sidebar/item_list.rs
index 0cd4a89d..1c0675b6 100644
--- a/src/session/sidebar/item_list.rs
+++ b/src/session/sidebar/item_list.rs
@@ -1,6 +1,11 @@
 use gtk::{gio, glib, prelude::*, subclass::prelude::*};
 
-use crate::session::{room::RoomType, room_list::RoomList, sidebar::Category};
+use crate::session::{
+    content::ContentType,
+    room::RoomType,
+    room_list::RoomList,
+    sidebar::{Category, Entry},
+};
 
 mod imp {
     use once_cell::unsync::OnceCell;
@@ -9,7 +14,7 @@ mod imp {
 
     #[derive(Debug, Default)]
     pub struct ItemList {
-        pub list: OnceCell<[Category; 5]>,
+        pub list: OnceCell<[glib::Object; 6]>,
     }
 
     #[glib::object_subclass]
@@ -24,7 +29,7 @@ mod imp {
 
     impl ListModelImpl for ItemList {
         fn item_type(&self, _list_model: &Self::Type) -> glib::Type {
-            Category::static_type()
+            glib::Object::static_type()
         }
         fn n_items(&self, _list_model: &Self::Type) -> u32 {
             self.list.get().map(|l| l.len()).unwrap_or(0) as u32
@@ -61,14 +66,15 @@ impl ItemList {
         priv_
             .list
             .set([
-                Category::new(RoomType::Invited, room_list),
-                Category::new(RoomType::Favorite, room_list),
-                Category::new(RoomType::Normal, room_list),
-                Category::new(RoomType::LowPriority, room_list),
-                Category::new(RoomType::Left, room_list),
+                Entry::new(ContentType::Explore).upcast::<glib::Object>(),
+                Category::new(RoomType::Invited, room_list).upcast::<glib::Object>(),
+                Category::new(RoomType::Favorite, room_list).upcast::<glib::Object>(),
+                Category::new(RoomType::Normal, room_list).upcast::<glib::Object>(),
+                Category::new(RoomType::LowPriority, room_list).upcast::<glib::Object>(),
+                Category::new(RoomType::Left, room_list).upcast::<glib::Object>(),
             ])
             .unwrap();
 
-        self.items_changed(0, 0, 5);
+        self.items_changed(0, 0, 6);
     }
 }
diff --git a/src/session/sidebar/mod.rs b/src/session/sidebar/mod.rs
index 0b298088..e9cc2def 100644
--- a/src/session/sidebar/mod.rs
+++ b/src/session/sidebar/mod.rs
@@ -1,5 +1,7 @@
 mod category;
 mod category_row;
+mod entry;
+mod entry_row;
 mod item_list;
 mod room_row;
 mod row;
@@ -8,6 +10,8 @@ mod sidebar;
 
 pub use self::category::Category;
 use self::category_row::CategoryRow;
+pub use self::entry::Entry;
+use self::entry_row::EntryRow;
 pub use self::item_list::ItemList;
 use self::room_row::RoomRow;
 use self::row::Row;
diff --git a/src/session/sidebar/row.rs b/src/session/sidebar/row.rs
index 3a4629b5..9ff8ed80 100644
--- a/src/session/sidebar/row.rs
+++ b/src/session/sidebar/row.rs
@@ -3,7 +3,7 @@ use gtk::{glib, prelude::*, subclass::prelude::*};
 
 use crate::session::{
     room::Room,
-    sidebar::{Category, CategoryRow, RoomRow},
+    sidebar::{Category, CategoryRow, Entry, EntryRow, RoomRow},
 };
 
 mod imp {
@@ -151,6 +151,21 @@ impl Row {
                 if let Some(list_item) = self.parent() {
                     list_item.set_css_classes(&["room"]);
                 }
+            } else if let Some(entry) = item.downcast_ref::<Entry>() {
+                let child = if let Some(Ok(child)) = self.child().map(|w| w.downcast::<EntryRow>())
+                {
+                    child
+                } else {
+                    let child = EntryRow::new();
+                    self.set_child(Some(&child));
+                    child
+                };
+
+                child.set_entry(Some(entry.clone()));
+
+                if let Some(list_item) = self.parent() {
+                    list_item.set_css_classes(&["entry"]);
+                }
             } else {
                 panic!("Wrong row item: {:?}", item);
             }
diff --git a/src/session/sidebar/selection.rs b/src/session/sidebar/selection.rs
index d74a3f81..544b6bf2 100644
--- a/src/session/sidebar/selection.rs
+++ b/src/session/sidebar/selection.rs
@@ -1,6 +1,6 @@
 use gtk::{gio, glib, glib::clone, prelude::*, subclass::prelude::*};
 
-use crate::session::room::Room;
+use crate::session::{content::ContentType, room::Room, sidebar::Entry};
 
 mod imp {
     use super::*;
@@ -11,7 +11,7 @@ mod imp {
     pub struct Selection {
         pub model: RefCell<Option<gio::ListModel>>,
         pub selected: Cell<u32>,
-        pub selected_room: RefCell<Option<Room>>,
+        pub selected_item: RefCell<Option<glib::Object>>,
         pub signal_handler: RefCell<Option<glib::SignalHandlerId>>,
     }
 
@@ -51,10 +51,18 @@ mod imp {
                         glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
                     ),
                     glib::ParamSpec::new_object(
-                        "selected-room",
-                        "Selected Room",
-                        "The selected room",
-                        Room::static_type(),
+                        "selected-item",
+                        "Selected Item",
+                        "The selected item",
+                        glib::Object::static_type(),
+                        glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
+                    ),
+                    glib::ParamSpec::new_enum(
+                        "selected-type",
+                        "Selected Type",
+                        "The currently selected content type",
+                        ContentType::static_type(),
+                        ContentType::default() as i32,
                         glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
                     ),
                 ]
@@ -79,10 +87,8 @@ mod imp {
                     let selected = value.get().unwrap();
                     obj.set_selected(selected);
                 }
-                "selected-room" => {
-                    let selected_room = value.get().unwrap();
-                    obj.set_selected_room(selected_room);
-                }
+                "selected-item" => obj.set_selected_item(value.get().unwrap()),
+                "selected-type" => obj.set_selected_type(value.get().unwrap()),
                 _ => unimplemented!(),
             }
         }
@@ -91,7 +97,8 @@ mod imp {
             match pspec.name() {
                 "model" => obj.model().to_value(),
                 "selected" => obj.selected().to_value(),
-                "selected-room" => obj.selected_room().to_value(),
+                "selected-item" => obj.selected_item().to_value(),
+                "selected-type" => obj.selected_type().to_value(),
                 _ => unimplemented!(),
             }
         }
@@ -157,9 +164,77 @@ impl Selection {
         priv_.selected.get()
     }
 
-    pub fn selected_room(&self) -> Option<Room> {
+    pub fn selected_item(&self) -> Option<glib::Object> {
         let priv_ = imp::Selection::from_instance(self);
-        priv_.selected_room.borrow().clone()
+        priv_.selected_item.borrow().clone()
+    }
+
+    pub fn selected_type(&self) -> ContentType {
+        if let Some(item) = self.selected_item() {
+            if item.is::<Room>() {
+                return ContentType::Room;
+            } else if let Ok(entry) = item.downcast::<Entry>() {
+                return entry.type_();
+            }
+        }
+
+        ContentType::None
+    }
+
+    pub fn set_selected_type(&self, selected_type: ContentType) {
+        let priv_ = imp::Selection::from_instance(self);
+
+        if self.selected_type() == selected_type {
+            return;
+        }
+
+        match selected_type {
+            ContentType::None => self.set_selected_item(None),
+            ContentType::Room => {
+                if self
+                    .selected_item()
+                    .and_then(|item| item.downcast::<Room>().ok())
+                    .is_none()
+                {
+                    if let Some(model) = &*priv_.model.borrow() {
+                        for i in 0..model.n_items() {
+                            if let Some(room) = model
+                                .item(i)
+                                .and_then(|item| item.downcast::<gtk::TreeListRow>().ok())
+                                .and_then(|i| i.item())
+                                .and_then(|o| o.downcast::<Room>().ok())
+                            {
+                                self.set_selected_item(Some(room.upcast()));
+                                break;
+                            }
+                        }
+                    }
+                }
+            }
+            ContentType::Explore => {
+                if !self
+                    .selected_item()
+                    .and_then(|item| item.downcast::<Entry>().ok())
+                    .map_or(false, |entry| entry.type_() == selected_type)
+                {
+                    if let Some(model) = &*priv_.model.borrow() {
+                        for i in 0..model.n_items() {
+                            if let Some(entry) = model
+                                .item(i)
+                                .and_then(|item| item.downcast::<gtk::TreeListRow>().ok())
+                                .and_then(|i| i.item())
+                                .and_then(|o| o.downcast::<Entry>().ok())
+                            {
+                                if entry.type_() == selected_type {
+                                    self.set_selected_item(Some(entry.upcast()));
+                                    break;
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        };
     }
 
     pub fn set_model<P: IsA<gio::ListModel>>(&self, model: Option<&P>) {
@@ -202,9 +277,10 @@ impl Selection {
                 priv_.selected.replace(gtk::INVALID_LIST_POSITION);
                 self.notify("selected");
             }
-            if self.selected_room().is_some() {
-                priv_.selected_room.replace(None);
-                self.notify("selected-room");
+            if self.selected_item().is_some() {
+                priv_.selected_item.replace(None);
+                self.notify("selected-type");
+                self.notify("selected-item");
             }
 
             self.items_changed(0, n_items_before, 0);
@@ -221,13 +297,13 @@ impl Selection {
             return;
         }
 
-        let selected_room = self
+        let selected_item = self
             .model()
             .and_then(|m| m.item(position))
             .and_then(|o| o.downcast::<gtk::TreeListRow>().ok())
-            .and_then(|r| r.item())
-            .and_then(|o| o.downcast::<Room>().ok());
-        let selected = if selected_room.is_none() {
+            .and_then(|r| r.item());
+
+        let selected = if selected_item.is_none() {
             gtk::INVALID_LIST_POSITION
         } else {
             position
@@ -238,7 +314,7 @@ impl Selection {
         }
 
         priv_.selected.replace(selected);
-        priv_.selected_room.replace(selected_room);
+        priv_.selected_item.replace(selected_item);
 
         if old_selected == gtk::INVALID_LIST_POSITION {
             self.selection_changed(selected, 1);
@@ -251,14 +327,15 @@ impl Selection {
         }
 
         self.notify("selected");
-        self.notify("selected-room");
+        self.notify("selected-item");
+        self.notify("selected-type");
     }
 
-    pub fn set_selected_room(&self, room: Option<Room>) {
+    fn set_selected_item(&self, item: Option<glib::Object>) {
         let priv_ = imp::Selection::from_instance(self);
 
-        let selected_room = self.selected_room();
-        if selected_room == room {
+        let selected_item = self.selected_item();
+        if selected_item == item {
             return;
         }
 
@@ -266,15 +343,14 @@ impl Selection {
 
         let mut selected = gtk::INVALID_LIST_POSITION;
 
-        if room.is_some() {
+        if item.is_some() {
             if let Some(model) = self.model() {
                 for i in 0..model.n_items() {
-                    let r = model
+                    let current_item = model
                         .item(i)
                         .and_then(|o| o.downcast::<gtk::TreeListRow>().ok())
-                        .and_then(|r| r.item())
-                        .and_then(|o| o.downcast::<Room>().ok());
-                    if r == room {
+                        .and_then(|r| r.item());
+                    if current_item == item {
                         selected = i;
                         break;
                     }
@@ -282,7 +358,7 @@ impl Selection {
             }
         }
 
-        priv_.selected_room.replace(room);
+        priv_.selected_item.replace(item);
 
         if old_selected != selected {
             priv_.selected.replace(selected);
@@ -299,7 +375,8 @@ impl Selection {
             self.notify("selected");
         }
 
-        self.notify("selected-room");
+        self.notify("selected-item");
+        self.notify("selected-type");
     }
 
     fn items_changed_cb(&self, model: &gio::ListModel, position: u32, removed: u32, added: u32) {
@@ -308,9 +385,9 @@ impl Selection {
         let _guard = self.freeze_notify();
 
         let selected = self.selected();
-        let selected_room = self.selected_room();
+        let selected_item = self.selected_item();
 
-        if selected_room.is_none() || selected < position {
+        if selected_item.is_none() || selected < position {
             // unchanged
         } else if selected != gtk::INVALID_LIST_POSITION && selected >= position + removed {
             priv_.selected.replace(selected + added - removed);
@@ -322,12 +399,11 @@ impl Selection {
                     priv_.selected.replace(gtk::INVALID_LIST_POSITION);
                     self.notify("selected");
                 } else {
-                    let room = model
+                    let item = model
                         .item(position + i)
                         .and_then(|o| o.downcast::<gtk::TreeListRow>().ok())
-                        .and_then(|r| r.item())
-                        .and_then(|o| o.downcast::<Room>().ok());
-                    if room == selected_room {
+                        .and_then(|r| r.item());
+                    if item == selected_item {
                         // the item moved
                         if selected != position + i {
                             priv_.selected.replace(position + i);
diff --git a/src/session/sidebar/sidebar.rs b/src/session/sidebar/sidebar.rs
index 40d858dd..4b868516 100644
--- a/src/session/sidebar/sidebar.rs
+++ b/src/session/sidebar/sidebar.rs
@@ -2,8 +2,9 @@ use adw::subclass::prelude::BinImpl;
 use gtk::{gio, glib, prelude::*, subclass::prelude::*, CompositeTemplate};
 
 use crate::session::{
+    content::ContentType,
     room::Room,
-    sidebar::{Category, ItemList, RoomRow, Row, Selection},
+    sidebar::{Category, Entry, ItemList, RoomRow, Row, Selection},
     RoomList,
 };
 
@@ -18,6 +19,7 @@ mod imp {
     pub struct Sidebar {
         pub compact: Cell<bool>,
         pub selected_room: RefCell<Option<Room>>,
+        pub selected_type: Cell<ContentType>,
         #[template_child]
         pub headerbar: TemplateChild<adw::HeaderBar>,
         #[template_child]
@@ -68,6 +70,14 @@ mod imp {
                         Room::static_type(),
                         glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
                     ),
+                    glib::ParamSpec::new_enum(
+                        "selected-type",
+                        "Selected",
+                        "The type of item that is selected",
+                        ContentType::static_type(),
+                        ContentType::default() as i32,
+                        glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
+                    ),
                 ]
             });
 
@@ -94,6 +104,7 @@ mod imp {
                     let selected_room = value.get().unwrap();
                     obj.set_selected_room(selected_room);
                 }
+                "selected-type" => obj.set_selected_type(value.get().unwrap()),
                 _ => unimplemented!(),
             }
         }
@@ -102,6 +113,7 @@ mod imp {
             match pspec.name() {
                 "compact" => self.compact.get().to_value(),
                 "selected-room" => obj.selected_room().to_value(),
+                "selected-type" => obj.selected_type().to_value(),
                 _ => unimplemented!(),
             }
         }
@@ -126,6 +138,12 @@ mod imp {
                             row.set_expanded(!row.is_expanded());
                         } else if row.item().and_then(|o| o.downcast::<Room>().ok()).is_some() {
                             model.set_selected(pos);
+                        } else if row
+                            .item()
+                            .and_then(|o| o.downcast::<Entry>().ok())
+                            .is_some()
+                        {
+                            model.set_selected(pos);
                         }
                     }
                 }
@@ -147,6 +165,23 @@ impl Sidebar {
         glib::Object::new(&[]).expect("Failed to create Sidebar")
     }
 
+    pub fn selected_type(&self) -> ContentType {
+        let priv_ = imp::Sidebar::from_instance(self);
+        priv_.selected_type.get()
+    }
+
+    fn set_selected_type(&self, selected_type: ContentType) {
+        let priv_ = imp::Sidebar::from_instance(self);
+
+        if self.selected_type() == selected_type {
+            return;
+        }
+
+        priv_.selected_type.set(selected_type);
+
+        self.notify("selected-type");
+    }
+
     pub fn selected_room(&self) -> Option<Room> {
         let priv_ = imp::Sidebar::from_instance(self);
         priv_.selected_room.borrow().clone()
@@ -188,7 +223,11 @@ impl Sidebar {
                 .build();
 
             let selection = Selection::new(Some(&filter_model));
-            self.bind_property("selected-room", &selection, "selected-room")
+            self.bind_property("selected-room", &selection, "selected-item")
+                .flags(glib::BindingFlags::SYNC_CREATE | glib::BindingFlags::BIDIRECTIONAL)
+                .build();
+
+            self.bind_property("selected-type", &selection, "selected-type")
                 .flags(glib::BindingFlags::SYNC_CREATE | glib::BindingFlags::BIDIRECTIONAL)
                 .build();
 
@@ -206,7 +245,6 @@ impl Sidebar {
         }
 
         priv_.selected_room.replace(selected_room);
-
         self.notify("selected-room");
     }
 }


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