[fractal] power_level badges for users in room settings



commit d60ce58cf3ea98b60862df3d3e9384a86c4de80e
Author: Crom (Thibaut CHARLES) <CromFr gmail com>
Date:   Fri Mar 8 22:52:07 2019 +0100

    power_level badges for users in room settings
    
    Used Avatar::circle badge parameter in MemberBox
    
    rustfmt
    
    Show power level next to the matrix user ID
    
    Fix mxid & power level forcing window width
    
    new design closer to @bertob's proposal
    
    Gap around avatar's badge
    
    Power level roles translation
    
    css-themed avatar's colored dot
    
    Reduced badge height

 fractal-gtk/res/app.css                  |  19 ++++
 fractal-gtk/src/appop/account.rs         |   2 +-
 fractal-gtk/src/appop/user.rs            |   4 +-
 fractal-gtk/src/widgets/avatar.rs        | 150 +++++++++++++++----------------
 fractal-gtk/src/widgets/member.rs        |  28 +++---
 fractal-gtk/src/widgets/members_list.rs  |  92 ++++++++++++++++---
 fractal-gtk/src/widgets/message.rs       |   8 +-
 fractal-gtk/src/widgets/mod.rs           |   3 +-
 fractal-gtk/src/widgets/room.rs          |   2 +-
 fractal-gtk/src/widgets/room_settings.rs |  11 ++-
 fractal-gtk/src/widgets/roomrow.rs       |   4 +-
 11 files changed, 205 insertions(+), 118 deletions(-)
---
diff --git a/fractal-gtk/res/app.css b/fractal-gtk/res/app.css
index bd38d5f6..4680520d 100644
--- a/fractal-gtk/res/app.css
+++ b/fractal-gtk/res/app.css
@@ -299,3 +299,22 @@ stack.titlebar:not(headerbar) > box > separator {
   margin-bottom: 8px;
   color: @theme_selected_bg_color;
 }
+
+
+.badge {
+  border-radius: 5px;
+  padding: 0.1em 5px;
+  font-size: 0.8em;
+}
+.badge-circle {
+  border-radius: 99999px;
+}
+.badge-gold {
+  background-color: #E5A50A;
+}
+.badge-silver {
+  background-color: #99A8B0;
+}
+.badge-grey {
+  background-color: #D9D9D9;
+}
\ No newline at end of file
diff --git a/fractal-gtk/src/appop/account.rs b/fractal-gtk/src/appop/account.rs
index 9116420c..c589bb02 100644
--- a/fractal-gtk/src/appop/account.rs
+++ b/fractal-gtk/src/appop/account.rs
@@ -456,7 +456,7 @@ impl AppOp {
         avatar.add(&w);
 
         let uid = self.uid.clone().unwrap_or_default();
-        let data = w.circle(uid.clone(), self.username.clone(), 100);
+        let data = w.circle(uid.clone(), self.username.clone(), 100, None, None);
         download_to_cache(self.backend.clone(), uid.clone(), data.clone());
 
         /* FIXME: hack to make the avatar drawing area clickable*/
diff --git a/fractal-gtk/src/appop/user.rs b/fractal-gtk/src/appop/user.rs
index 88306a2d..8a000fc8 100644
--- a/fractal-gtk/src/appop/user.rs
+++ b/fractal-gtk/src/appop/user.rs
@@ -52,7 +52,7 @@ impl AppOp {
 
             let w = widgets::Avatar::avatar_new(Some(40));
             let uid = self.uid.clone().unwrap_or_default();
-            let data = w.circle(uid.clone(), self.username.clone(), 40);
+            let data = w.circle(uid.clone(), self.username.clone(), 40, None, None);
             download_to_cache(self.backend.clone(), uid.clone(), data.clone());
 
             avatar.add(&w);
@@ -66,7 +66,7 @@ impl AppOp {
             Some(_) => {
                 let w = widgets::Avatar::avatar_new(Some(24));
                 let uid = self.uid.clone().unwrap_or_default();
-                let data = w.circle(uid.clone(), self.username.clone(), 24);
+                let data = w.circle(uid.clone(), self.username.clone(), 24, None, None);
                 download_to_cache(self.backend.clone(), uid.clone(), data.clone());
 
                 eb.add(&w);
diff --git a/fractal-gtk/src/widgets/avatar.rs b/fractal-gtk/src/widgets/avatar.rs
index 39350102..c2efc757 100644
--- a/fractal-gtk/src/widgets/avatar.rs
+++ b/fractal-gtk/src/widgets/avatar.rs
@@ -11,7 +11,13 @@ use gtk;
 use gtk::prelude::*;
 pub use gtk::DrawingArea;
 
-pub type Avatar = gtk::Box;
+pub enum AvatarBadgeColor {
+    Gold,
+    Silver,
+    Grey,
+}
+
+pub type Avatar = gtk::Overlay;
 
 pub struct AvatarData {
     uid: String,
@@ -39,13 +45,20 @@ impl AvatarData {
 }
 
 pub trait AvatarExt {
-    fn avatar_new(size: Option<i32>) -> gtk::Box;
+    fn avatar_new(size: Option<i32>) -> gtk::Overlay;
     fn clean(&self);
     fn create_da(&self, size: Option<i32>) -> DrawingArea;
-    fn circle(&self, uid: String, username: Option<String>, size: i32) -> Rc<RefCell<AvatarData>>;
+    fn circle(
+        &self,
+        uid: String,
+        username: Option<String>,
+        size: i32,
+        badge: Option<AvatarBadgeColor>,
+        badge_size: Option<i32>,
+    ) -> Rc<RefCell<AvatarData>>;
 }
 
-impl AvatarExt for gtk::Box {
+impl AvatarExt for gtk::Overlay {
     fn clean(&self) {
         for ch in self.get_children().iter() {
             self.remove(ch);
@@ -57,14 +70,14 @@ impl AvatarExt for gtk::Box {
 
         let s = size.unwrap_or(40);
         da.set_size_request(s, s);
-        self.pack_start(&da, true, true, 0);
+        self.add(&da);
         self.show_all();
 
         da
     }
 
-    fn avatar_new(size: Option<i32>) -> gtk::Box {
-        let b = gtk::Box::new(gtk::Orientation::Horizontal, 0);
+    fn avatar_new(size: Option<i32>) -> gtk::Overlay {
+        let b = gtk::Overlay::new();
         b.create_da(size);
         b.show_all();
         if let Some(style) = b.get_style_context() {
@@ -73,8 +86,20 @@ impl AvatarExt for gtk::Box {
 
         b
     }
-
-    fn circle(&self, uid: String, username: Option<String>, size: i32) -> Rc<RefCell<AvatarData>> {
+    /// # Arguments
+    /// * `uid` - Matrix ID
+    /// * `username` - Full name
+    /// * `size` - Size of the avatar
+    /// * `badge_color` - Badge color. None for no badge
+    /// * `badge_size` - Badge size. None for size / 3
+    fn circle(
+        &self,
+        uid: String,
+        username: Option<String>,
+        size: i32,
+        badge_color: Option<AvatarBadgeColor>,
+        badge_size: Option<i32>,
+    ) -> Rc<RefCell<AvatarData>> {
         self.clean();
         let da = self.create_da(Some(size));
         let path = cache_path(&uid).unwrap_or_default();
@@ -90,6 +115,25 @@ impl AvatarExt for gtk::Box {
         let fallback = letter_avatar::generate::new(uid.clone(), username, size as f64)
             .expect("this function should never fail");
 
+        // Power level badge setup
+        let has_badge = badge_color.is_some();
+        let badge_size = badge_size.unwrap_or(size / 3);
+        if let Some(color) = badge_color {
+            let badge = gtk::Box::new(gtk::Orientation::Vertical, 0);
+            badge.set_size_request(badge_size, badge_size);
+            badge.set_valign(gtk::Align::Start);
+            badge.set_halign(gtk::Align::End);
+            if let Some(style) = badge.get_style_context() {
+                style.add_class("badge-circle");
+                style.add_class(match color {
+                    AvatarBadgeColor::Gold => "badge-gold",
+                    AvatarBadgeColor::Silver => "badge-silver",
+                    AvatarBadgeColor::Grey => "badge-grey",
+                });
+            }
+            self.add_overlay(&badge);
+        }
+
         let data = AvatarData {
             uid: uid.clone(),
             username: uname,
@@ -109,19 +153,32 @@ impl AvatarExt for gtk::Box {
             g.set_antialias(cairo::Antialias::Best);
 
             {
-                let data = user_cache.borrow();
-                if let Some(ref pb) = data.cache {
-                    let context = da.get_style_context().unwrap();
-                    gtk::render_background(&context, g, 0.0, 0.0, width, height);
-
+                g.set_fill_rule(cairo::FillRule::EvenOdd);
+                g.arc(
+                    width / 2.0,
+                    height / 2.0,
+                    width.min(height) / 2.0,
+                    0.0,
+                    2.0 * PI,
+                );
+                if has_badge {
+                    g.clip_preserve();
+                    g.new_sub_path();
+                    let badge_radius = badge_size as f64 / 2.0;
                     g.arc(
-                        width / 2.0,
-                        height / 2.0,
-                        width.min(height) / 2.0,
+                        width - badge_radius,
+                        badge_radius,
+                        badge_radius * 1.4,
                         0.0,
                         2.0 * PI,
                     );
-                    g.clip();
+                }
+                g.clip();
+
+                let data = user_cache.borrow();
+                if let Some(ref pb) = data.cache {
+                    let context = da.get_style_context().unwrap();
+                    gtk::render_background(&context, g, 0.0, 0.0, width, height);
 
                     let hpos: f64 = (width - (pb.get_height()) as f64) / 2.0;
                     g.set_source_pixbuf(&pb, 0.0, hpos);
@@ -155,60 +212,3 @@ fn load_pixbuf(path: &str, size: i32) -> Option<Pixbuf> {
         None
     }
 }
-
-pub enum AdminColor {
-    Gold,
-    Silver,
-}
-
-pub fn admin_badge(kind: AdminColor, size: Option<i32>) -> gtk::DrawingArea {
-    let s = size.unwrap_or(10);
-
-    let da = DrawingArea::new();
-    da.set_size_request(s, s);
-
-    let color = match kind {
-        AdminColor::Gold => (237.0, 212.0, 0.0),
-        AdminColor::Silver => (186.0, 186.0, 186.0),
-    };
-
-    let border = match kind {
-        AdminColor::Gold => (107.0, 114.0, 0.0),
-        AdminColor::Silver => (137.0, 137.0, 137.0),
-    };
-
-    da.connect_draw(move |da, g| {
-        use std::f64::consts::PI;
-        g.set_antialias(cairo::Antialias::Best);
-
-        let width = s as f64;
-        let height = s as f64;
-
-        let context = da.get_style_context().unwrap();
-        gtk::render_background(&context, g, 0.0, 0.0, width, height);
-
-        g.set_source_rgba(color.0 / 256.0, color.1 / 256.0, color.2 / 256.0, 1.);
-        g.arc(
-            width / 2.0,
-            height / 2.0,
-            width.min(height) / 2.5,
-            0.0,
-            2.0 * PI,
-        );
-        g.fill();
-
-        g.set_source_rgba(border.0 / 256.0, border.1 / 256.0, border.2 / 256.0, 0.5);
-        g.arc(
-            width / 2.0,
-            height / 2.0,
-            width.min(height) / 2.5,
-            0.0,
-            2.0 * PI,
-        );
-        g.stroke();
-
-        Inhibit(false)
-    });
-
-    da
-}
diff --git a/fractal-gtk/src/widgets/member.rs b/fractal-gtk/src/widgets/member.rs
index c3447dc5..e4760fc8 100644
--- a/fractal-gtk/src/widgets/member.rs
+++ b/fractal-gtk/src/widgets/member.rs
@@ -54,10 +54,17 @@ impl<'a> MemberBox<'a> {
         }
 
         let avatar = widgets::Avatar::avatar_new(Some(globals::USERLIST_ICON_SIZE));
+        let badge = match self.op.member_level(self.member) {
+            100 => Some(widgets::AvatarBadgeColor::Gold),
+            50...100 => Some(widgets::AvatarBadgeColor::Silver),
+            _ => None,
+        };
         let data = avatar.circle(
             self.member.uid.clone(),
             Some(alias.clone()),
             globals::USERLIST_ICON_SIZE,
+            badge,
+            None,
         );
         let member_id = self.member.uid.clone();
         download_to_cache(backend.clone(), member_id.clone(), data.clone());
@@ -71,24 +78,7 @@ impl<'a> MemberBox<'a> {
             v.pack_start(&uid, true, true, 0);
         }
 
-        match self.op.member_level(self.member) {
-            100 => {
-                let overlay = gtk::Overlay::new();
-                overlay.add(&avatar);
-                overlay.add_overlay(&widgets::admin_badge(widgets::AdminColor::Gold, None));
-                w.add(&overlay);
-            }
-            50 => {
-                let overlay = gtk::Overlay::new();
-                overlay.add(&avatar);
-                overlay.add_overlay(&widgets::admin_badge(widgets::AdminColor::Silver, None));
-                w.add(&overlay);
-            }
-            _ => {
-                w.add(&avatar);
-            }
-        }
-
+        w.add(&avatar);
         w.add(&v);
 
         event_box.add(&w);
@@ -113,6 +103,8 @@ impl<'a> MemberBox<'a> {
             self.member.uid.clone(),
             Some(self.member.get_alias()),
             globals::PILL_ICON_SIZE,
+            None,
+            None,
         );
         let member_id = self.member.uid.clone();
         download_to_cache(backend.clone(), member_id.clone(), data.clone());
diff --git a/fractal-gtk/src/widgets/members_list.rs b/fractal-gtk/src/widgets/members_list.rs
index b55f3009..43a1a40c 100644
--- a/fractal-gtk/src/widgets/members_list.rs
+++ b/fractal-gtk/src/widgets/members_list.rs
@@ -1,5 +1,6 @@
 use fractal_api::clone;
 use std::cell::RefCell;
+use std::collections::hash_map::HashMap;
 use std::rc::Rc;
 
 use glib::signal;
@@ -9,7 +10,7 @@ use gtk::prelude::*;
 use crate::i18n::i18n;
 use crate::types::Member;
 use crate::widgets;
-use crate::widgets::avatar::AvatarExt;
+use crate::widgets::avatar::{AvatarBadgeColor, AvatarExt};
 
 #[derive(Debug, Clone)]
 pub struct MembersList {
@@ -17,15 +18,21 @@ pub struct MembersList {
     search_entry: gtk::SearchEntry,
     error: gtk::Label,
     members: Vec<Member>,
+    power_levels: HashMap<String, i32>,
 }
 
 impl MembersList {
-    pub fn new(m: Vec<Member>, entry: gtk::SearchEntry) -> MembersList {
+    pub fn new(
+        m: Vec<Member>,
+        power_levels: HashMap<String, i32>,
+        entry: gtk::SearchEntry,
+    ) -> MembersList {
         MembersList {
             container: gtk::ListBox::new(),
             error: gtk::Label::new(None),
             members: m,
             search_entry: entry,
+            power_levels: power_levels,
         }
     }
 
@@ -35,7 +42,11 @@ impl MembersList {
         let b = gtk::Box::new(gtk::Orientation::Vertical, 0);
         b.set_hexpand(true);
         b.pack_start(&self.container, true, true, 0);
-        add_rows(self.container.clone(), self.members.clone());
+        add_rows(
+            self.container.clone(),
+            self.members.clone(),
+            self.power_levels.clone(),
+        );
         self.error
             .get_style_context()?
             .add_class("no_member_search");
@@ -105,11 +116,11 @@ impl MembersList {
     }
 }
 
-fn create_row(member: Member) -> Option<gtk::ListBoxRow> {
+fn create_row(member: Member, power_level: Option<i32>) -> Option<gtk::ListBoxRow> {
     let row = gtk::ListBoxRow::new();
     row.connect_draw(clone!(member => move |w, _| {
         if w.get_child().is_none() {
-            w.add(&load_row_content(member.clone()));
+            w.add(&load_row_content(member.clone(), power_level));
         }
         gtk::Inhibit(false)
     }));
@@ -120,40 +131,93 @@ fn create_row(member: Member) -> Option<gtk::ListBoxRow> {
 }
 
 /* creating the row is quite slow, therefore we have a small delay when scrolling the members list */
-fn load_row_content(member: Member) -> gtk::Box {
+fn load_row_content(member: Member, power_level: Option<i32>) -> gtk::Box {
     let b = gtk::Box::new(gtk::Orientation::Horizontal, 12);
+
+    // Power level badge colour
+    let pl = power_level.unwrap_or_default();
+    let badge_color = match pl {
+        100 => Some(AvatarBadgeColor::Gold),
+        50...99 => Some(AvatarBadgeColor::Silver),
+        1...49 => Some(AvatarBadgeColor::Grey),
+        _ => None,
+    };
+
+    // Avatar
     let avatar = widgets::Avatar::avatar_new(Some(40));
-    avatar.circle(member.uid.clone(), member.alias.clone(), 40);
-    let user_box = gtk::Box::new(gtk::Orientation::Vertical, 0);
+    avatar.circle(
+        member.uid.clone(),
+        member.alias.clone(),
+        40,
+        badge_color,
+        None,
+    );
+
+    let user_box = gtk::Box::new(gtk::Orientation::Vertical, 0); // Name & badge + Matrix ID
+    let username_box = gtk::Box::new(gtk::Orientation::Horizontal, 0); // Name + badge
+
     let username = gtk::Label::new(Some(member.get_alias().as_str()));
-    let uid = gtk::Label::new(Some(member.uid.as_str()));
     username.set_xalign(0.);
     username.set_margin_end(5);
     username.set_ellipsize(pango::EllipsizeMode::End);
+    username_box.pack_start(&username, false, false, 0);
+
+    // Power level badge colour
+    let pl = power_level.unwrap_or_default();
+    if pl > 0 && pl <= 100 {
+        let badge_data = match pl {
+            100 => (i18n("Admin"), "badge-gold"),
+            50...99 => (i18n("Moderator"), "badge-silver"),
+            1...49 => (i18n("Privileged"), "badge-grey"),
+            _ => panic!(),
+        };
+
+        let badge_wid = gtk::Label::new(Some(format!("{} ({})", badge_data.0, pl).as_str()));
+        badge_wid.set_valign(gtk::Align::Center);
+        if let Some(style) = badge_wid.get_style_context() {
+            style.add_class("badge");
+            style.add_class(badge_data.1);
+        }
+        username_box.pack_start(&badge_wid, false, false, 0);
+    }
+
+    // matrix ID + power level
+    let uid = gtk::Label::new(Some(member.uid.as_str()));
     uid.set_xalign(0.);
+    uid.set_line_wrap(true);
+    uid.set_line_wrap_mode(pango::WrapMode::Char);
     if let Some(style) = uid.get_style_context() {
         style.add_class("small-font");
         style.add_class("dim-label");
     }
+
     b.set_margin_start(12);
     b.set_margin_end(12);
     b.set_margin_top(6);
     b.set_margin_bottom(6);
-    user_box.pack_start(&username, true, true, 0);
-    user_box.pack_start(&uid, false, false, 0);
+    user_box.pack_start(&username_box, true, true, 0);
+    user_box.pack_start(&uid, true, true, 0);
     /* we don't have this state yet
      * let state = gtk::Label::new();
      * user_box.pack_end(&state, true, true, 0); */
     b.pack_start(&avatar, false, true, 0);
-    b.pack_start(&user_box, false, true, 0);
+    b.pack_start(&user_box, true, true, 0);
     b.show_all();
     b
 }
 
-fn add_rows(container: gtk::ListBox, members: Vec<Member>) -> Option<usize> {
+fn add_rows(
+    container: gtk::ListBox,
+    members: Vec<Member>,
+    power_levels: HashMap<String, i32>,
+) -> Option<usize> {
     /* Load just enough members to fill atleast the visible list */
     for member in members.iter() {
-        container.insert(&create_row(member.clone())?, -1);
+        let power_level = match power_levels.get(&member.uid) {
+            Some(pl) => Some(*pl),
+            None => None,
+        };
+        container.insert(&create_row(member.clone(), power_level)?, -1);
     }
     None
 }
diff --git a/fractal-gtk/src/widgets/message.rs b/fractal-gtk/src/widgets/message.rs
index f2c2c9e5..e01090a6 100644
--- a/fractal-gtk/src/widgets/message.rs
+++ b/fractal-gtk/src/widgets/message.rs
@@ -166,7 +166,13 @@ impl MessageBox {
         let alias = msg.sender_name.clone();
         let avatar = widgets::Avatar::avatar_new(Some(globals::MSG_ICON_SIZE));
 
-        let data = avatar.circle(uid.clone(), alias.clone(), globals::MSG_ICON_SIZE);
+        let data = avatar.circle(
+            uid.clone(),
+            alias.clone(),
+            globals::MSG_ICON_SIZE,
+            None,
+            None,
+        );
         if let Some(name) = alias {
             self.username.set_text(&name);
         } else {
diff --git a/fractal-gtk/src/widgets/mod.rs b/fractal-gtk/src/widgets/mod.rs
index 04c6bbf8..81b422f1 100644
--- a/fractal-gtk/src/widgets/mod.rs
+++ b/fractal-gtk/src/widgets/mod.rs
@@ -23,9 +23,8 @@ mod sourceview_entry;
 pub use self::address::Address;
 pub use self::address::AddressType;
 pub use self::autocomplete::Autocomplete;
-pub use self::avatar::admin_badge;
-pub use self::avatar::AdminColor;
 pub use self::avatar::Avatar;
+pub use self::avatar::AvatarBadgeColor;
 pub use self::avatar::AvatarData;
 pub use self::avatar::AvatarExt;
 pub use self::divider::NewMessageDivider;
diff --git a/fractal-gtk/src/widgets/room.rs b/fractal-gtk/src/widgets/room.rs
index f60866fb..ccc960af 100644
--- a/fractal-gtk/src/widgets/room.rs
+++ b/fractal-gtk/src/widgets/room.rs
@@ -47,7 +47,7 @@ impl<'a> RoomBox<'a> {
         let widget_box = gtk::Box::new(gtk::Orientation::Horizontal, 0);
 
         let avatar = widgets::Avatar::avatar_new(Some(AVATAR_SIZE));
-        avatar.circle(room.id.clone(), room.name.clone(), AVATAR_SIZE);
+        avatar.circle(room.id.clone(), room.name.clone(), AVATAR_SIZE, None, None);
         widget_box.pack_start(&avatar, false, false, 18);
 
         let details_box = gtk::Box::new(gtk::Orientation::Vertical, 6);
diff --git a/fractal-gtk/src/widgets/room_settings.rs b/fractal-gtk/src/widgets/room_settings.rs
index b983983c..927f3f8b 100644
--- a/fractal-gtk/src/widgets/room_settings.rs
+++ b/fractal-gtk/src/widgets/room_settings.rs
@@ -415,7 +415,13 @@ impl RoomSettings {
         }
 
         let image = widgets::Avatar::avatar_new(Some(100));
-        let data = image.circle(self.room.id.clone(), self.room.name.clone(), 100);
+        let data = image.circle(
+            self.room.id.clone(),
+            self.room.name.clone(),
+            100,
+            None,
+            None,
+        );
         download_to_cache(self.backend.clone(), self.room.id.clone(), data);
 
         if edit {
@@ -616,7 +622,8 @@ impl RoomSettings {
             )
             .as_str(),
         );
-        let list = widgets::MembersList::new(members.clone(), entry);
+        let list =
+            widgets::MembersList::new(members.clone(), self.room.power_levels.clone(), entry);
         let w = list.create()?;
         b.add(&w);
         self.members_list = Some(list);
diff --git a/fractal-gtk/src/widgets/roomrow.rs b/fractal-gtk/src/widgets/roomrow.rs
index 4cb65ae7..3f471b0e 100644
--- a/fractal-gtk/src/widgets/roomrow.rs
+++ b/fractal-gtk/src/widgets/roomrow.rs
@@ -65,7 +65,7 @@ impl RoomRow {
             notifications.hide();
         }
 
-        icon.circle(room.id.clone(), Some(name), ICON_SIZE);
+        icon.circle(room.id.clone(), Some(name), ICON_SIZE, None, None);
 
         let rr = RoomRow {
             room,
@@ -130,7 +130,7 @@ impl RoomRow {
         let name = self.room.name.clone().unwrap_or("...".to_string());
 
         self.icon
-            .circle(self.room.id.clone(), Some(name), ICON_SIZE);
+            .circle(self.room.id.clone(), Some(name), ICON_SIZE, None, None);
     }
 
     pub fn widget(&self) -> gtk::ListBoxRow {


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