diff --git a/data/resources/resources.gresource.xml b/data/resources/resources.gresource.xml
index ca7c77b3..088f12e9 100644
--- a/data/resources/resources.gresource.xml
+++ b/data/resources/resources.gresource.xml
@@ -33,6 +33,7 @@
ui/spinner-button.ui
ui/in-app-notification.ui
ui/components-avatar.ui
+ ui/avatar-with-selection.ui
style.css
icons/scalable/actions/send-symbolic.svg
icons/scalable/status/welcome.svg
diff --git a/data/resources/style.css b/data/resources/style.css
index 08e2583f..f9486b67 100644
--- a/data/resources/style.css
+++ b/data/resources/style.css
@@ -55,6 +55,16 @@
background-color: alpha(@theme_bg_color, 0.2);
}
+.selected-avatar avatar {
+ border: 2px solid @accent_bg_color;
+}
+
+.blue-checkmark {
+ color: @accent_bg_color;
+ border-radius: 9999px;
+ background-color: white;
+}
+
/* Login */
.login {
min-width: 250px;
diff --git a/data/resources/ui/avatar-with-selection.ui b/data/resources/ui/avatar-with-selection.ui
new file mode 100644
index 00000000..aa9cae12
--- /dev/null
+++ b/data/resources/ui/avatar-with-selection.ui
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
diff --git a/data/resources/ui/user-entry-row.ui b/data/resources/ui/user-entry-row.ui
index 611fc607..c1c558fc 100644
--- a/data/resources/ui/user-entry-row.ui
+++ b/data/resources/ui/user-entry-row.ui
@@ -5,7 +5,7 @@
10
-
+
40
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 222b35dd..2086295f 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -7,6 +7,7 @@ data/org.gnome.FractalNext.metainfo.xml.in.in
# UI files
data/resources/ui/add_account.ui
data/resources/ui/components-avatar.ui
+data/resources/ui/avatar-with-selection.ui
data/resources/ui/content-divider-row.ui
data/resources/ui/content-item-row-menu.ui
data/resources/ui/content-item.ui
@@ -67,6 +68,7 @@ src/session/room/item.rs
src/session/room/mod.rs
src/session/room/timeline.rs
src/session/sidebar/account_switcher/add_account.rs
+src/session/sidebar/account_switcher/avatar_with_selection.rs
src/session/sidebar/account_switcher/item.rs
src/session/sidebar/account_switcher/mod.rs
src/session/sidebar/account_switcher/user_entry.rs
diff --git a/src/meson.build b/src/meson.build
index b72383c2..94e8dd32 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -70,6 +70,7 @@ sources = files(
'session/sidebar/room_row.rs',
'session/sidebar/selection.rs',
'session/sidebar/account_switcher/add_account.rs',
+ 'session/sidebar/account_switcher/avatar_with_selection.rs',
'session/sidebar/account_switcher/item.rs',
'session/sidebar/account_switcher/mod.rs',
'session/sidebar/account_switcher/user_entry.rs',
diff --git a/src/session/mod.rs b/src/session/mod.rs
index 436fa2f6..b888ccf0 100644
--- a/src/session/mod.rs
+++ b/src/session/mod.rs
@@ -452,7 +452,9 @@ impl Session {
pub fn set_logged_in_users(&self, sessions_stack_pages: &SelectionModel) {
let priv_ = &imp::Session::from_instance(self);
- priv_.sidebar.set_logged_in_users(sessions_stack_pages);
+ priv_
+ .sidebar
+ .set_logged_in_users(sessions_stack_pages, self);
}
}
diff --git a/src/session/sidebar/account_switcher/avatar_with_selection.rs b/src/session/sidebar/account_switcher/avatar_with_selection.rs
new file mode 100644
index 00000000..827d5e66
--- /dev/null
+++ b/src/session/sidebar/account_switcher/avatar_with_selection.rs
@@ -0,0 +1,131 @@
+use adw::subclass::prelude::*;
+use gtk::{glib, prelude::*, subclass::prelude::*, CompositeTemplate};
+
+use crate::components::Avatar;
+use crate::session::Avatar as AvatarItem;
+
+mod imp {
+ use super::*;
+ use glib::subclass::InitializingObject;
+ use once_cell::sync::Lazy;
+
+ #[derive(Debug, Default, CompositeTemplate)]
+ #[template(resource = "/org/gnome/FractalNext/avatar-with-selection.ui")]
+ pub struct AvatarWithSelection {
+ #[template_child]
+ pub child_avatar: TemplateChild,
+ #[template_child]
+ pub checkmark: TemplateChild,
+ }
+
+ #[glib::object_subclass]
+ impl ObjectSubclass for AvatarWithSelection {
+ const NAME: &'static str = "AvatarWithSelection";
+ type Type = super::AvatarWithSelection;
+ type ParentType = adw::Bin;
+
+ fn class_init(klass: &mut Self::Class) {
+ Avatar::static_type();
+ Self::bind_template(klass);
+ }
+
+ fn instance_init(obj: &InitializingObject) {
+ obj.init_template();
+ }
+ }
+
+ impl ObjectImpl for AvatarWithSelection {
+ fn properties() -> &'static [glib::ParamSpec] {
+ static PROPERTIES: Lazy> = Lazy::new(|| {
+ vec![
+ glib::ParamSpec::new_object(
+ "item",
+ "Item",
+ "The Avatar item displayed by this widget",
+ AvatarItem::static_type(),
+ glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
+ ),
+ glib::ParamSpec::new_int(
+ "size",
+ "Size",
+ "The size of the Avatar",
+ -1,
+ i32::MAX,
+ -1,
+ glib::ParamFlags::READWRITE,
+ ),
+ glib::ParamSpec::new_boolean(
+ "selected",
+ "Selected",
+ "Style helper for the inner Avatar",
+ false,
+ glib::ParamFlags::WRITABLE,
+ ),
+ ]
+ });
+
+ PROPERTIES.as_ref()
+ }
+
+ fn set_property(
+ &self,
+ obj: &Self::Type,
+ _id: usize,
+ value: &glib::Value,
+ pspec: &glib::ParamSpec,
+ ) {
+ match pspec.name() {
+ "item" => self.child_avatar.set_item(value.get().unwrap()),
+ "size" => self.child_avatar.set_size(value.get().unwrap()),
+ "selected" => obj.set_selected(value.get().unwrap()),
+ _ => unimplemented!(),
+ }
+ }
+
+ fn property(&self, _obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
+ match pspec.name() {
+ "item" => self.child_avatar.item().to_value(),
+ "size" => self.child_avatar.size().to_value(),
+ _ => unimplemented!(),
+ }
+ }
+ }
+
+ impl WidgetImpl for AvatarWithSelection {}
+ impl BinImpl for AvatarWithSelection {}
+}
+
+glib::wrapper! {
+ /// A widget displaying an `Avatar` for a `Room` or `User`.
+ pub struct AvatarWithSelection(ObjectSubclass)
+ @extends gtk::Widget, adw::Bin, @implements gtk::Accessible;
+}
+
+impl AvatarWithSelection {
+ pub fn new() -> Self {
+ glib::Object::new(&[]).expect("Failed to create AvatarWithSelection")
+ }
+
+ pub fn set_selected(&self, selected: bool) {
+ let priv_ = imp::AvatarWithSelection::from_instance(self);
+
+ priv_.checkmark.set_visible(selected);
+
+ if selected {
+ priv_.child_avatar.add_css_class("selected-avatar");
+ } else {
+ priv_.child_avatar.remove_css_class("selected-avatar");
+ }
+ }
+
+ pub fn avatar(&self) -> &Avatar {
+ let priv_ = imp::AvatarWithSelection::from_instance(self);
+ &priv_.child_avatar
+ }
+}
+
+impl Default for AvatarWithSelection {
+ fn default() -> Self {
+ Self::new()
+ }
+}
diff --git a/src/session/sidebar/account_switcher/item.rs b/src/session/sidebar/account_switcher/item.rs
index 332fa3e8..987fa82e 100644
--- a/src/session/sidebar/account_switcher/item.rs
+++ b/src/session/sidebar/account_switcher/item.rs
@@ -1,5 +1,6 @@
use super::add_account::AddAccountRow;
use super::user_entry::UserEntryRow;
+use crate::session::Session;
use gtk::{gio::ListStore, glib, prelude::*, subclass::prelude::*};
use std::convert::TryFrom;
@@ -110,9 +111,9 @@ impl ExtraItemObj {
}
}
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, PartialEq)]
pub enum Item {
- User(gtk::StackPage),
+ User(gtk::StackPage, bool),
Separator,
AddAccount,
}
@@ -132,15 +133,29 @@ impl TryFrom for Item {
fn try_from(object: glib::Object) -> Result {
object
.downcast::()
- .map(Self::User)
+ .map(|sp| Self::User(sp, false))
.or_else(|object| object.downcast::().map(|it| it.get().into()))
}
}
impl Item {
+ pub fn set_hint(self, session_root: Session) -> Self {
+ match self {
+ Self::User(session_page, _) => {
+ let hinted = session_root == session_page.child();
+ Self::User(session_page, hinted)
+ }
+ other => other,
+ }
+ }
+
pub fn build_widget(&self) -> gtk::Widget {
match self {
- Self::User(ref session_page) => UserEntryRow::new(session_page).upcast(),
+ Self::User(ref session_page, hinted) => {
+ let user_entry = UserEntryRow::new(session_page);
+ user_entry.set_hint(hinted.clone());
+ user_entry.upcast()
+ }
Self::Separator => gtk::Separator::new(gtk::Orientation::Vertical).upcast(),
Self::AddAccount => AddAccountRow::new().upcast(),
}
diff --git a/src/session/sidebar/account_switcher/mod.rs b/src/session/sidebar/account_switcher/mod.rs
index e1e5db0b..d4f92ee2 100644
--- a/src/session/sidebar/account_switcher/mod.rs
+++ b/src/session/sidebar/account_switcher/mod.rs
@@ -1,6 +1,6 @@
use gtk::{
gio::{self, ListModel, ListStore},
- glib,
+ glib::{self, clone},
prelude::*,
subclass::prelude::*,
CompositeTemplate, SelectionModel,
@@ -8,8 +8,10 @@ use gtk::{
use std::convert::TryFrom;
use super::account_switcher::item::{ExtraItemObj, Item as AccountSwitcherItem};
+use crate::session::Session;
pub mod add_account;
+pub mod avatar_with_selection;
pub mod item;
pub mod user_entry;
@@ -50,7 +52,7 @@ mod imp {
.map(AccountSwitcherItem::try_from)
.and_then(Result::ok)
.map(|item| match item {
- AccountSwitcherItem::User(session_page) => {
+ AccountSwitcherItem::User(session_page, _) => {
let session_widget = session_page.child();
session_widget
.parent()
@@ -65,37 +67,6 @@ mod imp {
_ => {}
});
});
-
- // There is no permanent stuff to take care of,
- // so only bind and unbind are connected.
- let ref factory = gtk::SignalListItemFactory::new();
- factory.connect_bind(|_, list_item| {
- list_item.set_selectable(false);
- let child = list_item
- .item()
- .map(AccountSwitcherItem::try_from)
- .and_then(Result::ok)
- .as_ref()
- .map(|item| {
- match item {
- AccountSwitcherItem::Separator => {
- list_item.set_activatable(false);
- }
- _ => {}
- }
-
- item
- })
- .map(AccountSwitcherItem::build_widget);
-
- list_item.set_child(child.as_ref());
- });
-
- factory.connect_unbind(|_, list_item| {
- list_item.set_child(gtk::NONE_WIDGET);
- });
-
- self.entries.set_factory(Some(factory));
}
}
@@ -109,9 +80,47 @@ glib::wrapper! {
}
impl AccountSwitcher {
- pub fn set_logged_in_users(&self, sessions_stack_pages: &SelectionModel) {
+ pub fn set_logged_in_users(
+ &self,
+ sessions_stack_pages: &SelectionModel,
+ session_root: &Session,
+ ) {
let entries = imp::AccountSwitcher::from_instance(self).entries.get();
+ // There is no permanent stuff to take care of,
+ // so only bind and unbind are connected.
+ let ref factory = gtk::SignalListItemFactory::new();
+ factory.connect_bind(clone!(@weak session_root => move |_, list_item| {
+ list_item.set_selectable(false);
+ let child = list_item
+ .item()
+ .map(AccountSwitcherItem::try_from)
+ .and_then(Result::ok)
+ .map(|item| {
+ // Given that all the account switchers are built per-session widget
+ // there is no need for callbacks or data bindings; just set the hint
+ // when building the entries and they will show correctly marked in
+ // each session widget.
+ let item = item.set_hint(session_root);
+
+ if item == AccountSwitcherItem::Separator {
+ list_item.set_activatable(false);
+ }
+
+ item
+ })
+ .as_ref()
+ .map(AccountSwitcherItem::build_widget);
+
+ list_item.set_child(child.as_ref());
+ }));
+
+ factory.connect_unbind(|_, list_item| {
+ list_item.set_child(gtk::NONE_WIDGET);
+ });
+
+ entries.set_factory(Some(factory));
+
let ref end_items = ExtraItemObj::list_store();
let ref items_split = ListStore::new(ListModel::static_type());
items_split.append(sessions_stack_pages);
diff --git a/src/session/sidebar/account_switcher/user_entry.rs b/src/session/sidebar/account_switcher/user_entry.rs
index 6edeaf14..613df8c8 100644
--- a/src/session/sidebar/account_switcher/user_entry.rs
+++ b/src/session/sidebar/account_switcher/user_entry.rs
@@ -1,4 +1,4 @@
-use crate::components::Avatar;
+use super::avatar_with_selection::AvatarWithSelection;
use adw::subclass::prelude::BinImpl;
use gtk::{self, glib, prelude::*, subclass::prelude::*, CompositeTemplate};
@@ -12,7 +12,7 @@ mod imp {
#[template(resource = "/org/gnome/FractalNext/user-entry-row.ui")]
pub struct UserEntryRow {
#[template_child]
- pub avatar_component: TemplateChild,
+ pub account_avatar: TemplateChild,
#[template_child]
pub display_name: TemplateChild,
#[template_child]
@@ -27,7 +27,7 @@ mod imp {
type ParentType = adw::Bin;
fn class_init(klass: &mut Self::Class) {
- Avatar::static_type();
+ AvatarWithSelection::static_type();
Self::bind_template(klass);
}
@@ -39,13 +39,22 @@ mod imp {
impl ObjectImpl for UserEntryRow {
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy> = Lazy::new(|| {
- vec![glib::ParamSpec::new_object(
- "session-page",
- "Session StackPage",
- "The stack page of the session that this entry represents",
- gtk::StackPage::static_type(),
- glib::ParamFlags::READWRITE,
- )]
+ vec![
+ glib::ParamSpec::new_object(
+ "session-page",
+ "Session StackPage",
+ "The stack page of the session that this entry represents",
+ gtk::StackPage::static_type(),
+ glib::ParamFlags::READWRITE,
+ ),
+ glib::ParamSpec::new_boolean(
+ "hint",
+ "Selection hint",
+ "The hint of the session that owns the account switcher which this entry belongs to",
+ false,
+ glib::ParamFlags::WRITABLE | glib::ParamFlags::CONSTRUCT_ONLY,
+ ),
+ ]
});
PROPERTIES.as_ref()
@@ -53,7 +62,7 @@ mod imp {
fn set_property(
&self,
- _obj: &Self::Type,
+ obj: &Self::Type,
_id: usize,
value: &glib::Value,
pspec: &glib::ParamSpec,
@@ -63,6 +72,7 @@ mod imp {
let session_page = value.get().unwrap();
self.session_page.replace(Some(session_page));
}
+ "hint" => obj.set_hint(value.get().unwrap()),
_ => unimplemented!(),
}
}
@@ -88,4 +98,13 @@ impl UserEntryRow {
pub fn new(session_page: >k::StackPage) -> Self {
glib::Object::new(&[("session-page", session_page)]).expect("Failed to create UserEntryRow")
}
+
+ pub fn set_hint(&self, hinted: bool) {
+ let priv_ = imp::UserEntryRow::from_instance(self);
+
+ priv_.account_avatar.set_selected(hinted);
+ priv_
+ .display_name
+ .set_css_classes(if hinted { &["bold"] } else { &[] });
+ }
}
diff --git a/src/session/sidebar/mod.rs b/src/session/sidebar/mod.rs
index 324a0d47..76262b61 100644
--- a/src/session/sidebar/mod.rs
+++ b/src/session/sidebar/mod.rs
@@ -23,6 +23,7 @@ use gtk::{gio, glib, prelude::*, subclass::prelude::*, CompositeTemplate, Select
use crate::session::content::ContentType;
use crate::session::room::Room;
use crate::session::RoomList;
+use crate::session::Session;
use account_switcher::AccountSwitcher;
mod imp {
@@ -268,10 +269,14 @@ impl Sidebar {
self.notify("selected-room");
}
- pub fn set_logged_in_users(&self, sessions_stack_pages: &SelectionModel) {
+ pub fn set_logged_in_users(
+ &self,
+ sessions_stack_pages: &SelectionModel,
+ session_root: &Session,
+ ) {
imp::Sidebar::from_instance(self)
.account_switcher
- .set_logged_in_users(sessions_stack_pages);
+ .set_logged_in_users(sessions_stack_pages, session_root);
}
}