[fractal/fractal-next] Reimplement sidebar using TreeListModel and session::room::Room



commit f352fc3cffa3f1ebf534637c67492f8268036ac2
Author: Julian Sparber <julian sparber net>
Date:   Wed Apr 28 14:57:33 2021 +0200

    Reimplement sidebar using TreeListModel and session::room::Room

 data/resources/resources.gresource.xml     |   4 +-
 data/resources/style.css                   |   8 +-
 data/resources/ui/session.ui               |   1 +
 data/resources/ui/sidebar-category-item.ui |  52 -----
 data/resources/ui/sidebar-category-row.ui  |  25 ---
 data/resources/ui/sidebar-item.ui          |  21 +++
 data/resources/ui/sidebar-room-item.ui     |  30 ---
 data/resources/ui/sidebar.ui               |  20 +-
 src/meson.build                            |   6 +-
 src/session/mod.rs                         |   3 +-
 src/session/sidebar/category.rs            | 293 -----------------------------
 src/session/sidebar/category_list.rs       | 110 -----------
 src/session/sidebar/category_row.rs        | 115 -----------
 src/session/sidebar/mod.rs                 | 161 +---------------
 src/session/sidebar/room.rs                | 194 -------------------
 src/session/sidebar/room_row.rs            | 235 +++++++++++++----------
 src/session/sidebar/row.rs                 | 130 +++++++++++++
 src/session/sidebar/sidebar.rs             | 133 +++++++++++++
 18 files changed, 436 insertions(+), 1105 deletions(-)
---
diff --git a/data/resources/resources.gresource.xml b/data/resources/resources.gresource.xml
index fd8d12a0..aae78ef5 100644
--- a/data/resources/resources.gresource.xml
+++ b/data/resources/resources.gresource.xml
@@ -6,9 +6,7 @@
     <file compressed="true" preprocess="xml-stripblanks" alias="login.ui">ui/login.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" alias="session.ui">ui/session.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" alias="sidebar.ui">ui/sidebar.ui</file>
-    <file compressed="true" preprocess="xml-stripblanks" 
alias="sidebar-category-item.ui">ui/sidebar-category-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-room-item.ui">ui/sidebar-room-item.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-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">style.css</file>
diff --git a/data/resources/style.css b/data/resources/style.css
index 85ea4858..0360bab4 100644
--- a/data/resources/style.css
+++ b/data/resources/style.css
@@ -21,10 +21,6 @@
 }
 
 /* Sidebar */
-.sidebar .navigation-sidebar row {  
-  padding: 6px 12px;
-}
-
 .sidebar row .dim-label {  
   padding: 6px 12px;
   font-size: 0.8em;
@@ -35,8 +31,8 @@
   font-weight: bold;
 }
 
-.sidebar .view {
-  padding: 0px;
+.sidebar indent  {
+ -gtk-icon-size: 0px;
 }
 
 .sidebar row .notification_count {
diff --git a/data/resources/ui/session.ui b/data/resources/ui/session.ui
index 33b35007..eb778f0d 100644
--- a/data/resources/ui/session.ui
+++ b/data/resources/ui/session.ui
@@ -6,6 +6,7 @@
         <child>
           <object class="Sidebar" id="sidebar">
             <property name="compact" bind-source="session" bind-property="folded" bind-flags="sync-create" />
+            <property name="categories" bind-source="Session" bind-property="categories" 
bind-flags="sync-create" />
           </object>
         </child>
         <child>
diff --git a/data/resources/ui/sidebar-item.ui b/data/resources/ui/sidebar-item.ui
new file mode 100644
index 00000000..8697f3d0
--- /dev/null
+++ b/data/resources/ui/sidebar-item.ui
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="GtkListItem">
+    <property name="child">
+      <!-- TODO: we need to implement our own Expander: 
https://gitlab.gnome.org/GNOME/fractal/-/issues/749-->
+      <object class="GtkTreeExpander" id="expander">
+        <binding name="list-row">
+          <lookup name="item">GtkListItem</lookup>
+        </binding>
+        <property name="child">
+          <object class="SidebarRow">
+            <property name="hexpand">True</property>
+            <binding name="item">
+                <lookup name="item">expander</lookup>
+            </binding>
+          </object>
+        </property>
+      </object>
+    </property>
+  </template>
+</interface>
diff --git a/data/resources/ui/sidebar.ui b/data/resources/ui/sidebar.ui
index 3bf94c9f..ae7e0f49 100644
--- a/data/resources/ui/sidebar.ui
+++ b/data/resources/ui/sidebar.ui
@@ -52,18 +52,14 @@
             <property name="vexpand">True</property>
             <property name="hscrollbar-policy">never</property>
             <property name="child">
-             <object class="GtkListView" id="listview">
-               <property name="model">
-                 <object class="GtkNoSelection">
-                   <property name="model">
-                     <object class="CategoryList" />
-                   </property>
-                 </object>
-               </property>
-               <property name="factory">
-                 <object class="GtkBuilderListItemFactory">
-                   <property name="resource">/org/gnome/FractalNext/sidebar-category-item.ui</property>
-                 </object>
+              <object class="GtkListView" id="listview">
+                <style>
+                  <class name="navigation-sidebar"/>
+                </style>
+                <property name="factory">
+                  <object class="GtkBuilderListItemFactory">
+                    <property name="resource">/org/gnome/FractalNext/sidebar-item.ui</property>
+                  </object>
                 </property>
                 <accessibility>
                   <property name="label" translatable="yes">Sidebar</property>
diff --git a/src/meson.build b/src/meson.build
index 853bf061..1e192571 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -40,11 +40,9 @@ sources = files(
   'session/room/room.rs',
   'session/room/timeline.rs',
   'session/sidebar/mod.rs',
-  'session/sidebar/category_row.rs',
+  'session/sidebar/row.rs',
   'session/sidebar/room_row.rs',
-  'session/sidebar/category.rs',
-  'session/sidebar/category_list.rs',
-  'session/sidebar/room.rs',
+  'session/sidebar/sidebar.rs',
 )
 
 custom_target(
diff --git a/src/session/mod.rs b/src/session/mod.rs
index 6938445d..c19ca8aa 100644
--- a/src/session/mod.rs
+++ b/src/session/mod.rs
@@ -298,8 +298,7 @@ impl Session {
     /// Note that the `Store` currently doesn't store all events, therefore, we arn't really
     /// loading much via this function.
     pub fn load(&self) {
-        let priv_ = imp::Session::from_instance(self);
-        priv_.sidebar.load(&priv_.client.get().unwrap());
+        // TODO: load rooms from the store before the sync completes
     }
 
     /// Returns and consumes the `error` that was generated when the session failed to login,
diff --git a/src/session/sidebar/mod.rs b/src/session/sidebar/mod.rs
index f3b83dbf..17e19937 100644
--- a/src/session/sidebar/mod.rs
+++ b/src/session/sidebar/mod.rs
@@ -1,158 +1,7 @@
-mod category;
-mod category_list;
-mod category_row;
-mod room;
 mod room_row;
+mod row;
+mod sidebar;
 
-use self::category::{Category, CategoryName};
-use self::category_list::CategoryList;
-use self::category_row::SidebarCategoryRow;
-use self::room::{HighlightFlags, Room};
-use self::room_row::SidebarRoomRow;
-
-use adw;
-use adw::subclass::prelude::BinImpl;
-use gtk::subclass::prelude::*;
-use gtk::{self, prelude::*};
-use gtk::{glib, glib::clone, glib::SyncSender, CompositeTemplate};
-use matrix_sdk::{identifiers::RoomId, Client};
-
-mod imp {
-    use super::*;
-    use glib::subclass::InitializingObject;
-    use std::cell::Cell;
-
-    #[derive(Debug, CompositeTemplate)]
-    #[template(resource = "/org/gnome/FractalNext/sidebar.ui")]
-    pub struct Sidebar {
-        pub compact: Cell<bool>,
-        #[template_child]
-        pub headerbar: TemplateChild<adw::HeaderBar>,
-        #[template_child]
-        pub listview: TemplateChild<gtk::ListView>,
-    }
-
-    #[glib::object_subclass]
-    impl ObjectSubclass for Sidebar {
-        const NAME: &'static str = "Sidebar";
-        type Type = super::Sidebar;
-        type ParentType = adw::Bin;
-
-        fn new() -> Self {
-            Self {
-                compact: Cell::new(false),
-                listview: TemplateChild::default(),
-                headerbar: TemplateChild::default(),
-            }
-        }
-
-        fn class_init(klass: &mut Self::Class) {
-            CategoryList::static_type();
-            SidebarRoomRow::static_type();
-            SidebarCategoryRow::static_type();
-            Self::bind_template(klass);
-        }
-
-        fn instance_init(obj: &InitializingObject<Self>) {
-            obj.init_template();
-        }
-    }
-
-    impl ObjectImpl for Sidebar {
-        fn properties() -> &'static [glib::ParamSpec] {
-            use once_cell::sync::Lazy;
-            static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
-                vec![glib::ParamSpec::new_boolean(
-                    "compact",
-                    "Compact",
-                    "Wheter a compact view is used or not",
-                    false,
-                    glib::ParamFlags::READWRITE,
-                )]
-            });
-
-            PROPERTIES.as_ref()
-        }
-
-        fn set_property(
-            &self,
-            _obj: &Self::Type,
-            _id: usize,
-            value: &glib::Value,
-            pspec: &glib::ParamSpec,
-        ) {
-            match pspec.name() {
-                "compact" => {
-                    let compact = value
-                        .get()
-                        .expect("type conformity checked by `Object::set_property`");
-                    self.compact.set(compact);
-                }
-                _ => unimplemented!(),
-            }
-        }
-
-        fn property(&self, _obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
-            match pspec.name() {
-                "compact" => self.compact.get().to_value(),
-                _ => unimplemented!(),
-            }
-        }
-    }
-
-    impl WidgetImpl for Sidebar {}
-    impl BinImpl for Sidebar {}
-}
-
-glib::wrapper! {
-    pub struct Sidebar(ObjectSubclass<imp::Sidebar>)
-        @extends gtk::Widget, adw::Bin, @implements gtk::Accessible;
-}
-
-impl Sidebar {
-    pub fn new() -> Self {
-        glib::Object::new(&[]).expect("Failed to create Sidebar")
-    }
-
-    /// Sets up the required channel to recive async updates from the `Client`
-    pub fn setup_channel(&self) -> SyncSender<RoomId> {
-        let (sender, receiver) = glib::MainContext::sync_channel::<RoomId>(Default::default(), 100);
-
-        receiver.attach(
-            None,
-            clone!(@weak self as obj => @default-panic, move |room_id| {
-                obj.get_list_model().update(&room_id);
-                glib::Continue(true)
-            }),
-        );
-        sender
-    }
-
-    /// Loads the state from the `Store`
-    pub fn load(&self, client: &Client) {
-        let list = self.get_list_model();
-        // TODO: Add list for user defined categories e.g. favorite
-        let invited = Category::new(client.clone(), CategoryName::Invited);
-        let joined = Category::new(client.clone(), CategoryName::Normal);
-        let left = Category::new(client.clone(), CategoryName::Left);
-
-        invited.append_batch(client.invited_rooms().into_iter().map(Into::into).collect());
-        joined.append_batch(client.joined_rooms().into_iter().map(Into::into).collect());
-        left.append_batch(client.left_rooms().into_iter().map(Into::into).collect());
-
-        list.append_batch(&[invited, joined, left]);
-    }
-
-    fn get_list_model(&self) -> CategoryList {
-        imp::Sidebar::from_instance(self)
-            .listview
-            .model()
-            .unwrap()
-            .downcast::<gtk::NoSelection>()
-            .unwrap()
-            .model()
-            .unwrap()
-            .downcast::<CategoryList>()
-            .unwrap()
-    }
-}
+use self::room_row::RoomRow;
+use self::row::Row;
+pub use self::sidebar::Sidebar;
diff --git a/src/session/sidebar/room_row.rs b/src/session/sidebar/room_row.rs
index ef1dd252..6183564c 100644
--- a/src/session/sidebar/room_row.rs
+++ b/src/session/sidebar/room_row.rs
@@ -1,17 +1,20 @@
-use crate::session::sidebar::HighlightFlags;
-use adw;
 use adw::subclass::prelude::BinImpl;
-use gtk::subclass::prelude::*;
-use gtk::{self, prelude::*};
-use gtk::{gio, glib, CompositeTemplate};
+use gtk::{glib, glib::clone, prelude::*, subclass::prelude::*, CompositeTemplate};
+
+use crate::session::room::{HighlightFlags, Room};
 
 mod imp {
     use super::*;
-    use glib::subclass::InitializingObject;
+    use glib::{subclass::InitializingObject, SignalHandlerId};
+    use once_cell::sync::Lazy;
+    use std::cell::RefCell;
 
-    #[derive(Debug, CompositeTemplate)]
+    #[derive(Debug, Default, CompositeTemplate)]
     #[template(resource = "/org/gnome/FractalNext/sidebar-room-row.ui")]
-    pub struct SidebarRoomRow {
+    pub struct RoomRow {
+        pub room: RefCell<Option<Room>>,
+        pub bindings: RefCell<Vec<glib::Binding>>,
+        pub signal_handler: RefCell<Option<SignalHandlerId>>,
         #[template_child]
         pub avatar: TemplateChild<adw::Avatar>,
         #[template_child]
@@ -21,19 +24,11 @@ mod imp {
     }
 
     #[glib::object_subclass]
-    impl ObjectSubclass for SidebarRoomRow {
+    impl ObjectSubclass for RoomRow {
         const NAME: &'static str = "SidebarRoomRow";
-        type Type = super::SidebarRoomRow;
+        type Type = super::RoomRow;
         type ParentType = adw::Bin;
 
-        fn new() -> Self {
-            Self {
-                avatar: TemplateChild::default(),
-                display_name: TemplateChild::default(),
-                notification_count: TemplateChild::default(),
-            }
-        }
-
         fn class_init(klass: &mut Self::Class) {
             Self::bind_template(klass);
         }
@@ -43,43 +38,16 @@ mod imp {
         }
     }
 
-    impl ObjectImpl for SidebarRoomRow {
+    impl ObjectImpl for RoomRow {
         fn properties() -> &'static [glib::ParamSpec] {
-            use once_cell::sync::Lazy;
             static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
-                vec![
-                    glib::ParamSpec::new_object(
-                        "avatar",
-                        "Avatar",
-                        "The url of the avatar of this room",
-                        gio::LoadableIcon::static_type(),
-                        glib::ParamFlags::WRITABLE,
-                    ),
-                    glib::ParamSpec::new_string(
-                        "display-name",
-                        "Display Name",
-                        "The display name of this room",
-                        None,
-                        glib::ParamFlags::WRITABLE,
-                    ),
-                    glib::ParamSpec::new_flags(
-                        "highlight",
-                        "Highlight",
-                        "What type of highligh this room needs",
-                        HighlightFlags::static_type(),
-                        HighlightFlags::default().bits(),
-                        glib::ParamFlags::WRITABLE,
-                    ),
-                    glib::ParamSpec::new_uint64(
-                        "notification-count",
-                        "Notification count",
-                        "The notification count of this room",
-                        std::u64::MIN,
-                        std::u64::MAX,
-                        0,
-                        glib::ParamFlags::WRITABLE,
-                    ),
-                ]
+                vec![glib::ParamSpec::new_object(
+                    "room",
+                    "Room",
+                    "The room of this row",
+                    Room::static_type(),
+                    glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
+                )]
             });
 
             PROPERTIES.as_ref()
@@ -87,75 +55,136 @@ mod imp {
 
         fn set_property(
             &self,
-            _obj: &Self::Type,
+            obj: &Self::Type,
             _id: usize,
             value: &glib::Value,
             pspec: &glib::ParamSpec,
         ) {
             match pspec.name() {
-                "avatar" => {
-                    let _avatar = value
-                        .get::<Option<gio::LoadableIcon>>()
-                        .expect("type conformity checked by `Object::set_property`");
-                    // TODO: set custom avatar https://gitlab.gnome.org/exalm/libadwaita/-/issues/29
-                }
-                "display-name" => {
-                    let display_name = value
-                        .get()
-                        .expect("type conformity checked by `Object::set_property`");
-                    self.display_name.set_label(display_name);
-                }
-                "highlight" => {
-                    let flags = value
-                        .get::<HighlightFlags>()
-                        .expect("type conformity checked by `Object::set_property`");
-                    match flags {
-                        HighlightFlags::NONE => {
-                            self.notification_count.remove_css_class("highlight");
-                            self.display_name.remove_css_class("bold");
-                        }
-                        HighlightFlags::HIGHLIGHT => {
-                            self.notification_count.add_css_class("highlight");
-                            self.display_name.remove_css_class("bold");
-                        }
-                        HighlightFlags::BOLD => {
-                            self.display_name.add_css_class("bold");
-                            self.notification_count.remove_css_class("highlight");
-                        }
-                        HighlightFlags::HIGHLIGHT_BOLD => {
-                            self.notification_count.add_css_class("highlight");
-                            self.display_name.add_css_class("bold");
-                        }
-                        _ => {}
-                    }
-                }
-                "notification-count" => {
-                    let count = value
-                        .get::<u64>()
-                        .expect("type conformity checked by `Object::set_property`");
-                    self.notification_count.set_label(&count.to_string());
-                    self.notification_count.set_visible(count > 0);
+                "room" => {
+                    let room = value.get().unwrap();
+                    obj.set_room(room);
                 }
                 _ => unimplemented!(),
             }
         }
 
-        fn constructed(&self, obj: &Self::Type) {
-            self.parent_constructed(obj);
+        fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
+            match pspec.name() {
+                "room" => obj.room().to_value(),
+                _ => unimplemented!(),
+            }
         }
     }
 
-    impl WidgetImpl for SidebarRoomRow {}
-    impl BinImpl for SidebarRoomRow {}
+    impl WidgetImpl for RoomRow {}
+    impl BinImpl for RoomRow {}
 }
 
 glib::wrapper! {
-    pub struct SidebarRoomRow(ObjectSubclass<imp::SidebarRoomRow>)
+    pub struct RoomRow(ObjectSubclass<imp::RoomRow>)
         @extends gtk::Widget, adw::Bin, @implements gtk::Accessible;
 }
 
-impl SidebarRoomRow {
+impl RoomRow {
     pub fn new() -> Self {
-        glib::Object::new(&[]).expect("Failed to create SidebarRoomRow")
+        glib::Object::new(&[]).expect("Failed to create RoomRow")
+    }
+
+    pub fn room(&self) -> Option<Room> {
+        let priv_ = imp::RoomRow::from_instance(&self);
+        priv_.room.borrow().clone()
+    }
+
+    pub fn set_room(&self, room: Option<Room>) {
+        let priv_ = imp::RoomRow::from_instance(&self);
+
+        if self.room() == room {
+            return;
+        }
+
+        if let Some(room) = priv_.room.take() {
+            if let Some(id) = priv_.signal_handler.take() {
+                room.disconnect(id);
+            }
+        }
+
+        let mut bindings = priv_.bindings.borrow_mut();
+        while let Some(binding) = bindings.pop() {
+            binding.unbind();
+        }
+
+        if let Some(ref room) = room {
+            // TODO: set custom avatar https://gitlab.gnome.org/exalm/libadwaita/-/issues/29
+
+            let display_name_binding = room
+                .bind_property("display-name", &priv_.display_name.get(), "label")
+                .flags(glib::BindingFlags::SYNC_CREATE)
+                .build()
+                .unwrap();
+
+            let notification_count_binding = room
+                .bind_property(
+                    "notification-count",
+                    &priv_.notification_count.get(),
+                    "label",
+                )
+                .flags(glib::BindingFlags::SYNC_CREATE)
+                .build()
+                .unwrap();
+            let notification_count_vislbe_binding = room
+                .bind_property(
+                    "notification-count",
+                    &priv_.notification_count.get(),
+                    "visible",
+                )
+                .flags(glib::BindingFlags::SYNC_CREATE)
+                .transform_from(|_, value| Some((value.get::<u64>().unwrap() > 0).to_value()))
+                .build()
+                .unwrap();
+
+            priv_.signal_handler.replace(Some(room.connect_notify_local(
+                Some("highlight"),
+                clone!(@weak self as obj => move |_, _| {
+                        obj.set_highlight();
+                }),
+            )));
+
+            self.set_highlight();
+
+            bindings.append(&mut vec![
+                display_name_binding,
+                notification_count_binding,
+                notification_count_vislbe_binding,
+            ]);
+        }
+
+        priv_.room.replace(room);
+        self.notify("room");
+    }
+
+    fn set_highlight(&self) {
+        let priv_ = imp::RoomRow::from_instance(&self);
+        if let Some(room) = &*priv_.room.borrow() {
+            match room.highlight() {
+                HighlightFlags::NONE => {
+                    priv_.notification_count.remove_css_class("highlight");
+                    priv_.display_name.remove_css_class("bold");
+                }
+                HighlightFlags::HIGHLIGHT => {
+                    priv_.notification_count.add_css_class("highlight");
+                    priv_.display_name.remove_css_class("bold");
+                }
+                HighlightFlags::BOLD => {
+                    priv_.display_name.add_css_class("bold");
+                    priv_.notification_count.remove_css_class("highlight");
+                }
+                HighlightFlags::HIGHLIGHT_BOLD => {
+                    priv_.notification_count.add_css_class("highlight");
+                    priv_.display_name.add_css_class("bold");
+                }
+                _ => {}
+            };
+        }
     }
 }
diff --git a/src/session/sidebar/row.rs b/src/session/sidebar/row.rs
new file mode 100644
index 00000000..d0845c4c
--- /dev/null
+++ b/src/session/sidebar/row.rs
@@ -0,0 +1,130 @@
+use adw::{subclass::prelude::BinImpl, BinExt};
+use gtk::{glib, prelude::*, subclass::prelude::*};
+
+use crate::session::sidebar::RoomRow;
+use crate::session::{categories::Category, room::Room};
+
+mod imp {
+    use super::*;
+    use once_cell::sync::Lazy;
+    use std::cell::RefCell;
+
+    #[derive(Debug, Default)]
+    pub struct Row {
+        pub item: RefCell<Option<glib::Object>>,
+        pub binding: RefCell<Option<glib::Binding>>,
+    }
+
+    #[glib::object_subclass]
+    impl ObjectSubclass for Row {
+        const NAME: &'static str = "SidebarRow";
+        type Type = super::Row;
+        type ParentType = adw::Bin;
+    }
+
+    impl ObjectImpl for Row {
+        fn properties() -> &'static [glib::ParamSpec] {
+            static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
+                vec![glib::ParamSpec::new_object(
+                    "item",
+                    "Item",
+                    "The sidebar item of this row",
+                    glib::Object::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() {
+                "item" => {
+                    let item = value.get().unwrap();
+                    obj.set_item(item);
+                }
+                _ => unimplemented!(),
+            }
+        }
+
+        fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
+            match pspec.name() {
+                "item" => obj.item().to_value(),
+                _ => unimplemented!(),
+            }
+        }
+    }
+
+    impl WidgetImpl for Row {}
+    impl BinImpl for Row {}
+}
+
+glib::wrapper! {
+    pub struct Row(ObjectSubclass<imp::Row>)
+        @extends gtk::Widget, adw::Bin, @implements gtk::Accessible;
+}
+
+impl Row {
+    pub fn new() -> Self {
+        glib::Object::new(&[]).expect("Failed to create Row")
+    }
+
+    pub fn item(&self) -> Option<glib::Object> {
+        let priv_ = imp::Row::from_instance(&self);
+        priv_.item.borrow().clone()
+    }
+
+    pub fn set_item(&self, item: Option<glib::Object>) {
+        let priv_ = imp::Row::from_instance(&self);
+
+        if self.item() == item {
+            return;
+        }
+
+        if let Some(binding) = priv_.binding.take() {
+            binding.unbind();
+        }
+
+        if let Some(item) = item {
+            if let Some(category) = item.downcast_ref::<Category>() {
+                let child =
+                    if let Some(Ok(child)) = self.child().map(|w| w.downcast::<gtk::Label>()) {
+                        child
+                    } else {
+                        let child = gtk::Label::new(None);
+                        self.set_child(Some(&child));
+                        self.set_halign(gtk::Align::Start);
+                        child.add_css_class("dim-label");
+                        child
+                    };
+
+                let binding = category
+                    .bind_property("display-name", &child, "label")
+                    .flags(glib::BindingFlags::SYNC_CREATE)
+                    .build()
+                    .unwrap();
+
+                priv_.binding.replace(Some(binding));
+            } else if let Some(room) = item.downcast_ref::<Room>() {
+                let child = if let Some(Ok(child)) = self.child().map(|w| w.downcast::<RoomRow>()) {
+                    child
+                } else {
+                    let child = RoomRow::new();
+                    self.set_child(Some(&child));
+                    child
+                };
+
+                child.set_room(Some(room.clone()));
+            } else {
+                panic!("Wrong row item: {:?}", item);
+            }
+        }
+        self.notify("item");
+    }
+}
diff --git a/src/session/sidebar/sidebar.rs b/src/session/sidebar/sidebar.rs
new file mode 100644
index 00000000..5ddf2a9d
--- /dev/null
+++ b/src/session/sidebar/sidebar.rs
@@ -0,0 +1,133 @@
+use adw::subclass::prelude::BinImpl;
+use gtk::{gio, glib, glib::clone, prelude::*, subclass::prelude::*, CompositeTemplate};
+
+use crate::session::{
+    categories::Categories,
+    room::Room,
+    sidebar::{RoomRow, Row},
+};
+
+mod imp {
+    use super::*;
+    use glib::subclass::InitializingObject;
+    use once_cell::sync::Lazy;
+    use std::cell::Cell;
+
+    #[derive(Debug, Default, CompositeTemplate)]
+    #[template(resource = "/org/gnome/FractalNext/sidebar.ui")]
+    pub struct Sidebar {
+        pub compact: Cell<bool>,
+        #[template_child]
+        pub headerbar: TemplateChild<adw::HeaderBar>,
+        #[template_child]
+        pub listview: TemplateChild<gtk::ListView>,
+    }
+
+    #[glib::object_subclass]
+    impl ObjectSubclass for Sidebar {
+        const NAME: &'static str = "Sidebar";
+        type Type = super::Sidebar;
+        type ParentType = adw::Bin;
+
+        fn class_init(klass: &mut Self::Class) {
+            RoomRow::static_type();
+            Row::static_type();
+            Self::bind_template(klass);
+        }
+
+        fn instance_init(obj: &InitializingObject<Self>) {
+            obj.init_template();
+        }
+    }
+
+    impl ObjectImpl for Sidebar {
+        fn properties() -> &'static [glib::ParamSpec] {
+            static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
+                vec![
+                    glib::ParamSpec::new_boolean(
+                        "compact",
+                        "Compact",
+                        "Wheter a compact view is used or not",
+                        false,
+                        glib::ParamFlags::READWRITE,
+                    ),
+                    glib::ParamSpec::new_object(
+                        "categories",
+                        "Categories",
+                        "A list of rooms grouped into categories",
+                        Categories::static_type(),
+                        glib::ParamFlags::WRITABLE,
+                    ),
+                ]
+            });
+
+            PROPERTIES.as_ref()
+        }
+
+        fn set_property(
+            &self,
+            obj: &Self::Type,
+            _id: usize,
+            value: &glib::Value,
+            pspec: &glib::ParamSpec,
+        ) {
+            match pspec.name() {
+                "compact" => {
+                    let compact = value.get().unwrap();
+                    self.compact.set(compact);
+                }
+                "categories" => {
+                    let categories = value.get().unwrap();
+                    obj.set_categories(categories);
+                }
+                _ => unimplemented!(),
+            }
+        }
+
+        fn property(&self, _obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
+            match pspec.name() {
+                "compact" => self.compact.get().to_value(),
+                _ => unimplemented!(),
+            }
+        }
+    }
+
+    impl WidgetImpl for Sidebar {}
+    impl BinImpl for Sidebar {}
+}
+
+glib::wrapper! {
+    pub struct Sidebar(ObjectSubclass<imp::Sidebar>)
+        @extends gtk::Widget, adw::Bin, @implements gtk::Accessible;
+}
+
+impl Sidebar {
+    pub fn new() -> Self {
+        glib::Object::new(&[]).expect("Failed to create Sidebar")
+    }
+
+    pub fn set_categories(&self, categories: Option<Categories>) {
+        let priv_ = imp::Sidebar::from_instance(self);
+
+        if let Some(categories) = categories {
+            // TODO: hide empty categories
+            let tree_model = gtk::TreeListModel::new(&categories, false, true, |item| {
+                item.clone().downcast::<gio::ListModel>().ok()
+            });
+
+            // TODO: set filter based on the text in the search entry
+            let filter_model = gtk::FilterListModel::new(Some(&tree_model), gtk::NONE_FILTER);
+
+            let selection = gtk::SingleSelection::new(Some(&filter_model));
+            selection.connect_notify_local(Some("selected-item"), clone!(@weak self as obj => move |model, 
_| {
+                if let Some(room) = model.selected_item().and_then(|row| 
row.downcast_ref::<gtk::TreeListRow>().unwrap().item()).and_then(|o| o.downcast::<Room>().ok()) {
+                        obj.activate_action("session.show-room", 
Some(&room.matrix_room().room_id().as_str().to_variant()));
+                }
+            }));
+
+            priv_.listview.set_model(Some(&selection));
+        } else {
+            priv_.listview.set_model(gtk::NONE_SELECTION_MODEL);
+        }
+    }
+}


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