refactor avatar component
This commit is contained in:
@@ -299,9 +299,9 @@ impl Room {
|
||||
}
|
||||
|
||||
/// A stable seed for the room's generated avatar
|
||||
pub fn display_image_seed(&self, cx: &App) -> SharedString {
|
||||
pub fn display_image_seed(&self, cx: &App) -> String {
|
||||
if self.is_group() {
|
||||
SharedString::from(self.id.to_string())
|
||||
self.id.to_string()
|
||||
} else {
|
||||
self.display_member(cx).avatar_seed()
|
||||
}
|
||||
|
||||
@@ -641,10 +641,7 @@ impl CommunityPanel {
|
||||
.community
|
||||
.read_with(cx, |community, _cx| {
|
||||
let seed = community.id().to_hex();
|
||||
let avatar = match community.icon() {
|
||||
Some(path) => Avatar::from_source(path).seed(seed).large(),
|
||||
None => Avatar::new(None).seed(seed).large(),
|
||||
};
|
||||
let avatar = Avatar::from_source(community.icon()).seed(seed).large();
|
||||
|
||||
(community.name(), avatar)
|
||||
})
|
||||
@@ -762,10 +759,7 @@ impl Panel for CommunityPanel {
|
||||
self.community
|
||||
.read_with(cx, |community, _cx| {
|
||||
let seed = community.id().to_hex();
|
||||
let avatar = match community.icon() {
|
||||
Some(path) => Avatar::from_source(path).seed(seed).xsmall(),
|
||||
None => Avatar::new(None).seed(seed).xsmall(),
|
||||
};
|
||||
let avatar = Avatar::from_source(community.icon()).seed(seed).xsmall();
|
||||
|
||||
h_flex()
|
||||
.gap_1()
|
||||
|
||||
@@ -113,8 +113,8 @@ impl Person {
|
||||
}
|
||||
|
||||
/// A stable seed for this profile's generated avatar
|
||||
pub fn avatar_seed(&self) -> SharedString {
|
||||
SharedString::from(self.public_key().to_hex())
|
||||
pub fn avatar_seed(&self) -> String {
|
||||
self.public_key().to_hex()
|
||||
}
|
||||
|
||||
/// Get profile name
|
||||
|
||||
+24
-132
@@ -10,7 +10,10 @@ use crate::{Selectable, Sizable, Size, StyledExt};
|
||||
|
||||
/// Number of rows and columns in the generated pixel grid.
|
||||
const PIXEL_GRID: usize = 8;
|
||||
/// Probability that a cell in the left half of the grid is filled.
|
||||
/// Empty cells kept between the pattern and the avatar edge, so the art
|
||||
/// gathers in the center instead of filling the whole avatar.
|
||||
const MARGIN: usize = 1;
|
||||
/// Probability that a cell in the left half of the pattern area is filled.
|
||||
const FILL_PROBABILITY: f32 = 0.42;
|
||||
/// Probability that a filled cell uses the accent shade instead of the main color.
|
||||
const ACCENT_PROBABILITY: f32 = 0.25;
|
||||
@@ -33,19 +36,6 @@ pub(super) fn avatar_size(size: Size) -> AbsoluteLength {
|
||||
}
|
||||
|
||||
/// A deterministic, offline pixel-art avatar derived from a seed.
|
||||
///
|
||||
/// Use it for entities that have no profile picture: the same seed always
|
||||
/// renders the same pattern, so identities stay recognizable without a
|
||||
/// network round trip. The pattern is painted as geometry and cropped to a
|
||||
/// circle, at the same sizes as [`Avatar`].
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use ui::avatar::PixelAvatar;
|
||||
///
|
||||
/// PixelAvatar::new("alice");
|
||||
/// ```
|
||||
#[derive(IntoElement)]
|
||||
pub struct PixelAvatar {
|
||||
seed: u64,
|
||||
@@ -87,7 +77,7 @@ impl RenderOnce for PixelAvatar {
|
||||
move |bounds, seed, window, cx| {
|
||||
let theme = cx.theme();
|
||||
let main = Hsla {
|
||||
h: (theme.icon_accent.h + seed as f32 / u64::MAX as f32) % 1.,
|
||||
h: seed as f32 / u64::MAX as f32,
|
||||
s: 0.6,
|
||||
l: if theme.is_dark() { 0.6 } else { 0.45 },
|
||||
a: 1.,
|
||||
@@ -142,8 +132,11 @@ fn pixel_pattern(seed: u64) -> [u8; PIXEL_GRID * PIXEL_GRID] {
|
||||
let mut pattern = [0u8; PIXEL_GRID * PIXEL_GRID];
|
||||
let mut filled = 0usize;
|
||||
|
||||
for row in 0..PIXEL_GRID {
|
||||
for col in 0..PIXEL_GRID / 2 {
|
||||
let art_rows = PIXEL_GRID - 2 * MARGIN;
|
||||
let art_columns = PIXEL_GRID / 2 - MARGIN;
|
||||
|
||||
for row in MARGIN..PIXEL_GRID - MARGIN {
|
||||
for col in MARGIN..PIXEL_GRID / 2 {
|
||||
if rng.chance(FILL_PROBABILITY) {
|
||||
let accent = rng.chance(ACCENT_PROBABILITY);
|
||||
set_cell(&mut pattern, row, col, if accent { 2 } else { 1 });
|
||||
@@ -153,17 +146,17 @@ fn pixel_pattern(seed: u64) -> [u8; PIXEL_GRID * PIXEL_GRID] {
|
||||
}
|
||||
|
||||
if filled < MIN_FILLED {
|
||||
let half = PIXEL_GRID * PIXEL_GRID / 2;
|
||||
let start = (rng.next() % half as u64) as usize;
|
||||
let total = art_rows * art_columns;
|
||||
let start = (rng.next() % total as u64) as usize;
|
||||
|
||||
for offset in 0..half {
|
||||
for offset in 0..total {
|
||||
if filled >= MIN_FILLED {
|
||||
break;
|
||||
}
|
||||
|
||||
let ix = (start + offset) % half;
|
||||
let row = ix / (PIXEL_GRID / 2);
|
||||
let col = ix % (PIXEL_GRID / 2);
|
||||
let ix = (start + offset) % total;
|
||||
let row = MARGIN + ix / art_columns;
|
||||
let col = MARGIN + ix % art_columns;
|
||||
|
||||
if pattern[row * PIXEL_GRID + col] == 0 {
|
||||
set_cell(&mut pattern, row, col, 1);
|
||||
@@ -358,24 +351,12 @@ fn generated_avatar(seed: Option<&str>, size: Pixels) -> AnyElement {
|
||||
}
|
||||
|
||||
/// An element that renders a user avatar with customizable appearance options.
|
||||
///
|
||||
/// Entities without a picture still get a stable identity: the avatar falls
|
||||
/// back to a [`PixelAvatar`] seeded through [`Avatar::seed`], both when there
|
||||
/// is no picture and when the picture fails to load.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use ui::avatar::Avatar;
|
||||
///
|
||||
/// Avatar::new(None).seed("alice");
|
||||
/// ```
|
||||
#[derive(IntoElement)]
|
||||
pub struct Avatar {
|
||||
base: Div,
|
||||
picture: Option<ImageSource>,
|
||||
grayscale: bool,
|
||||
seed: Option<SharedString>,
|
||||
seed: Option<String>,
|
||||
style: StyleRefinement,
|
||||
size: Size,
|
||||
border_color: Option<Hsla>,
|
||||
@@ -384,16 +365,13 @@ pub struct Avatar {
|
||||
|
||||
impl Avatar {
|
||||
/// Creates an avatar for an entity whose profile picture may be missing.
|
||||
///
|
||||
/// Use [`Avatar::seed`] to choose the generated
|
||||
/// pixel avatar rendered when `picture` is `None`.
|
||||
pub fn new(picture: Option<SharedString>) -> Self {
|
||||
Self::from_picture(picture.map(ImageSource::from))
|
||||
Self::from_source(picture)
|
||||
}
|
||||
|
||||
/// Creates an avatar from an already-resolved source.
|
||||
pub fn from_source(picture: impl Into<ImageSource>) -> Self {
|
||||
Self::from_picture(Some(picture.into()))
|
||||
/// Creates an avatar from an already-resolved source, which may be missing.
|
||||
pub fn from_source(picture: Option<impl Into<ImageSource>>) -> Self {
|
||||
Self::from_picture(picture.map(Into::into))
|
||||
}
|
||||
|
||||
fn from_picture(picture: Option<ImageSource>) -> Self {
|
||||
@@ -410,33 +388,18 @@ impl Avatar {
|
||||
}
|
||||
|
||||
/// Sets the seed for the generated pixel avatar.
|
||||
///
|
||||
/// The seed should be a stable identifier of the entity the avatar
|
||||
/// represents, such as a public key.
|
||||
pub fn seed(mut self, seed: impl Into<SharedString>) -> Self {
|
||||
pub fn seed(mut self, seed: impl Into<String>) -> Self {
|
||||
self.seed = Some(seed.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Applies a grayscale filter to the avatar image.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use ui::avatar::Avatar;
|
||||
///
|
||||
/// Avatar::new(None).grayscale(true);
|
||||
/// ```
|
||||
pub fn grayscale(mut self, grayscale: bool) -> Self {
|
||||
self.grayscale = grayscale;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the border color of the avatar.
|
||||
///
|
||||
/// This might be used to match the border to the background color of
|
||||
/// the parent element to create the illusion of cropping another
|
||||
/// shape underneath (for example in face piles.)
|
||||
pub fn border_color(mut self, color: impl Into<Hsla>) -> Self {
|
||||
self.border_color = Some(color.into());
|
||||
self
|
||||
@@ -480,6 +443,7 @@ impl RenderOnce for Avatar {
|
||||
} else {
|
||||
px(0.)
|
||||
};
|
||||
|
||||
let image_size = avatar_size(self.size).to_pixels(window.rem_size());
|
||||
let container_size = image_size + border_width * 2.;
|
||||
|
||||
@@ -487,6 +451,7 @@ impl RenderOnce for Avatar {
|
||||
Some(picture) => {
|
||||
let seed = self.seed;
|
||||
let grayscale = self.grayscale;
|
||||
|
||||
img(picture)
|
||||
.size(image_size)
|
||||
.rounded_full()
|
||||
@@ -510,76 +475,3 @@ impl RenderOnce for Avatar {
|
||||
.child(content)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn pixel_patterns_are_symmetric_and_stable() {
|
||||
for seed in 0..50 {
|
||||
let pattern = pixel_pattern(seed);
|
||||
let filled = pattern.iter().filter(|&&cell| cell != 0).count();
|
||||
|
||||
assert!(
|
||||
filled >= MIN_FILLED * 2,
|
||||
"pattern too sparse for seed {seed}"
|
||||
);
|
||||
|
||||
for row in 0..PIXEL_GRID {
|
||||
for col in 0..PIXEL_GRID {
|
||||
assert_eq!(
|
||||
pattern[row * PIXEL_GRID + col],
|
||||
pattern[row * PIXEL_GRID + (PIXEL_GRID - 1 - col)],
|
||||
"asymmetric pattern for seed {seed} at ({row}, {col})"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for seed in [0, 1, 42, u64::MAX] {
|
||||
assert_eq!(pixel_pattern(seed), pixel_pattern(seed));
|
||||
}
|
||||
|
||||
assert_ne!(pixel_pattern(42), pixel_pattern(43));
|
||||
}
|
||||
|
||||
fn area(polygon: &[Point<Pixels>]) -> f32 {
|
||||
let mut sum: f32 = 0.;
|
||||
for (&a, &b) in polygon.iter().zip(polygon.iter().cycle().skip(1)) {
|
||||
sum += a.x.as_f32() * b.y.as_f32() - b.x.as_f32() * a.y.as_f32();
|
||||
}
|
||||
(sum / 2.).abs()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clipping_keeps_only_the_part_inside_the_circle() {
|
||||
let circle = circle_polygon(point(px(10.), px(10.)), 10.);
|
||||
let square = |left: f32, top: f32| {
|
||||
[
|
||||
point(px(left), px(top)),
|
||||
point(px(left + 4.), px(top)),
|
||||
point(px(left + 4.), px(top + 4.)),
|
||||
point(px(left), px(top + 4.)),
|
||||
]
|
||||
};
|
||||
|
||||
let inside = clip_polygon(&square(8., 8.), &circle);
|
||||
assert!((area(&inside) - 16.).abs() < 0.05, "area {}", area(&inside));
|
||||
|
||||
assert!(clip_polygon(&square(20., 20.), &circle).is_empty());
|
||||
|
||||
let straddling = clip_polygon(&square(0., 0.), &circle);
|
||||
for vertex in &straddling {
|
||||
let delta_x = vertex.x.as_f32() - 10.;
|
||||
let delta_y = vertex.y.as_f32() - 10.;
|
||||
assert!(
|
||||
delta_x.hypot(delta_y) <= 10. + 0.1,
|
||||
"clipped vertex outside the circle"
|
||||
);
|
||||
}
|
||||
|
||||
let area = area(&straddling);
|
||||
assert!(area > 0. && area < 16., "area {area}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ use gpui::{
|
||||
};
|
||||
use nostr_sdk::prelude::*;
|
||||
use person::PersonRegistry;
|
||||
use settings::AppSettings;
|
||||
use state::NostrRegistry;
|
||||
use theme::ActiveTheme;
|
||||
use ui::avatar::Avatar;
|
||||
@@ -21,8 +22,6 @@ use ui::input::{Input, InputEvent, InputState};
|
||||
use ui::nav::Nav;
|
||||
use ui::{Disableable, IconName, Sizable, WindowExtension, h_flex, v_flex};
|
||||
|
||||
use crate::sidebar::nav_avatar;
|
||||
|
||||
/// Maximum number of members a chat can have.
|
||||
const MAX_MEMBERS: usize = 3;
|
||||
|
||||
@@ -349,10 +348,14 @@ impl NewChat {
|
||||
Nav::new(SharedString::from(format!("contact-{}", start + offset)))
|
||||
.label(profile.name())
|
||||
.text_sm()
|
||||
.when_some(
|
||||
nav_avatar(Some(profile.avatar_seed()), profile.avatar(), cx),
|
||||
|this, avatar| this.prefix(avatar),
|
||||
)
|
||||
.when(!AppSettings::get_hide_avatar(cx), |this| {
|
||||
this.prefix(
|
||||
Avatar::from_source(profile.avatar())
|
||||
.seed(profile.avatar_seed())
|
||||
.small()
|
||||
.flex_shrink_0(),
|
||||
)
|
||||
})
|
||||
.on_click(cx.listener(move |this, _event, _window, cx| {
|
||||
this.toggle_member(&public_key, cx);
|
||||
}))
|
||||
@@ -380,9 +383,12 @@ impl NewChat {
|
||||
.bg(cx.theme().ghost_element_background_alt)
|
||||
.text_size(px(10.))
|
||||
.hover(|this| this.bg(cx.theme().ghost_element_hover))
|
||||
.when_some(profile.avatar(), |this, avatar| {
|
||||
this.child(Avatar::from_source(avatar).xsmall().flex_shrink_0())
|
||||
})
|
||||
.child(
|
||||
Avatar::from_source(profile.avatar())
|
||||
.seed(profile.avatar_seed())
|
||||
.xsmall()
|
||||
.flex_shrink_0(),
|
||||
)
|
||||
.child(div().max_w_32().truncate().child(profile.name()))
|
||||
.on_click(cx.listener(move |this, _event, _window, cx| {
|
||||
this.toggle_member(&public_key, cx);
|
||||
|
||||
@@ -13,9 +13,11 @@ use gpui::{
|
||||
use instant::Duration;
|
||||
use nostr_sdk::prelude::*;
|
||||
use person::PersonRegistry;
|
||||
use settings::AppSettings;
|
||||
use smallvec::{SmallVec, smallvec};
|
||||
use state::{FIND_DELAY, NostrRegistry};
|
||||
use theme::ActiveTheme;
|
||||
use ui::avatar::Avatar;
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::dock::{DockArea, DockPlacement, Panel, PanelEvent, PanelHandle};
|
||||
use ui::input::{Input, InputEvent, InputState};
|
||||
@@ -23,8 +25,6 @@ use ui::nav::Nav;
|
||||
use ui::notification::Notification;
|
||||
use ui::{Icon, IconName, Selectable, Sizable, StyledExt, WindowExtension, h_flex, v_flex};
|
||||
|
||||
use crate::sidebar::nav_avatar;
|
||||
|
||||
const INPUT_PLACEHOLDER: &str = "Find or start a conversation";
|
||||
|
||||
pub fn init(dock: WeakEntity<DockArea>, window: &mut Window, cx: &mut App) -> Entity<SearchPanel> {
|
||||
@@ -337,6 +337,7 @@ impl SearchPanel {
|
||||
range: Range<usize>,
|
||||
cx: &Context<Self>,
|
||||
) -> Vec<impl IntoElement + use<>> {
|
||||
let hide_avatar = AppSettings::get_hide_avatar(cx);
|
||||
let persons = PersonRegistry::global(cx);
|
||||
|
||||
// Get the results
|
||||
@@ -365,10 +366,14 @@ impl SearchPanel {
|
||||
.label(profile.name())
|
||||
.text_sm()
|
||||
.font_medium()
|
||||
.when_some(
|
||||
nav_avatar(Some(profile.avatar_seed()), profile.avatar(), cx),
|
||||
|this, avatar| this.prefix(avatar),
|
||||
)
|
||||
.when(!hide_avatar, |this| {
|
||||
this.prefix(
|
||||
Avatar::from_source(profile.avatar())
|
||||
.seed(profile.avatar_seed())
|
||||
.small()
|
||||
.flex_shrink_0(),
|
||||
)
|
||||
})
|
||||
.on_click(handler)
|
||||
.selected(selected)
|
||||
.into_any_element()
|
||||
@@ -382,6 +387,7 @@ impl SearchPanel {
|
||||
range: Range<usize>,
|
||||
cx: &Context<Self>,
|
||||
) -> Vec<impl IntoElement + use<>> {
|
||||
let hide_avatar = AppSettings::get_hide_avatar(cx);
|
||||
let persons = PersonRegistry::global(cx);
|
||||
|
||||
// Get the contact list
|
||||
@@ -410,10 +416,14 @@ impl SearchPanel {
|
||||
.label(profile.name().trim())
|
||||
.text_sm()
|
||||
.font_medium()
|
||||
.when_some(
|
||||
nav_avatar(Some(profile.avatar_seed()), profile.avatar(), cx),
|
||||
|this, avatar| this.prefix(avatar),
|
||||
)
|
||||
.when(!hide_avatar, |this| {
|
||||
this.prefix(
|
||||
Avatar::from_source(profile.avatar())
|
||||
.seed(profile.avatar_seed())
|
||||
.small()
|
||||
.flex_shrink_0(),
|
||||
)
|
||||
})
|
||||
.on_click(handler)
|
||||
.selected(selected)
|
||||
.into_any_element()
|
||||
|
||||
@@ -44,7 +44,7 @@ mod tab;
|
||||
mod utils;
|
||||
|
||||
use tab::SidebarTab;
|
||||
pub(crate) use utils::{nav_avatar, nav_icon, pick_banner};
|
||||
pub(crate) use utils::{nav_icon, pick_banner};
|
||||
|
||||
pub enum SidebarRow {
|
||||
Room { room: Entity<Room> },
|
||||
@@ -833,6 +833,8 @@ impl Sidebar {
|
||||
rows: &[SidebarRow],
|
||||
cx: &Context<Sidebar>,
|
||||
) -> Vec<AnyElement> {
|
||||
let hide_avatar = AppSettings::get_hide_avatar(cx);
|
||||
|
||||
rows.get(range.clone())
|
||||
.into_iter()
|
||||
.flatten()
|
||||
@@ -855,8 +857,13 @@ impl Sidebar {
|
||||
.label(name)
|
||||
.text_sm()
|
||||
.font_medium()
|
||||
.when_some(nav_avatar(Some(seed), picture, cx), |this, avatar| {
|
||||
this.prefix(avatar)
|
||||
.when(!hide_avatar, |this| {
|
||||
this.prefix(
|
||||
Avatar::from_source(picture)
|
||||
.seed(seed)
|
||||
.small()
|
||||
.flex_shrink_0(),
|
||||
)
|
||||
})
|
||||
.suffix(
|
||||
div()
|
||||
@@ -904,10 +911,14 @@ impl Sidebar {
|
||||
.label(person.name())
|
||||
.text_sm()
|
||||
.font_medium()
|
||||
.when_some(
|
||||
nav_avatar(Some(person.avatar_seed()), person.avatar(), cx),
|
||||
|this, avatar| this.prefix(avatar),
|
||||
)
|
||||
.when(!hide_avatar, |this| {
|
||||
this.prefix(
|
||||
Avatar::from_source(person.avatar())
|
||||
.seed(person.avatar_seed())
|
||||
.small()
|
||||
.flex_shrink_0(),
|
||||
)
|
||||
})
|
||||
.selected(selected)
|
||||
.on_click(move |_event, _window, cx| {
|
||||
if let Err(error) = sidebar.update(cx, |this, cx| {
|
||||
@@ -929,8 +940,13 @@ impl Sidebar {
|
||||
Nav::new(SharedString::from(format!("com-{index}")))
|
||||
.label(name)
|
||||
.text_sm()
|
||||
.when_some(nav_avatar(Some(seed), picture, cx), |this, avatar| {
|
||||
this.prefix(avatar)
|
||||
.when(!hide_avatar, |this| {
|
||||
this.prefix(
|
||||
Avatar::from_source(picture)
|
||||
.seed(seed)
|
||||
.small()
|
||||
.flex_shrink_0(),
|
||||
)
|
||||
})
|
||||
.on_click(move |_event, window, cx| {
|
||||
ui::dock::add_panel_to(
|
||||
@@ -1021,6 +1037,7 @@ impl Sidebar {
|
||||
}
|
||||
|
||||
fn member_row(&self, public_key: &PublicKey, cx: &App) -> AnyElement {
|
||||
let hide_avatar = AppSettings::get_hide_avatar(cx);
|
||||
let persons = PersonRegistry::global(cx);
|
||||
let person = persons.read(cx).get(public_key, cx);
|
||||
|
||||
@@ -1028,10 +1045,14 @@ impl Sidebar {
|
||||
.label(person.name())
|
||||
.text_sm()
|
||||
.font_medium()
|
||||
.when_some(
|
||||
nav_avatar(Some(person.avatar_seed()), person.avatar(), cx),
|
||||
|this, avatar| this.prefix(avatar),
|
||||
)
|
||||
.when(!hide_avatar, |this| {
|
||||
this.prefix(
|
||||
Avatar::from_source(person.avatar())
|
||||
.seed(person.avatar_seed())
|
||||
.small()
|
||||
.flex_shrink_0(),
|
||||
)
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,39 +1,9 @@
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{AnyElement, App, ImageSource, IntoElement, ParentElement, SharedString, Styled, px};
|
||||
use settings::AppSettings;
|
||||
use gpui::{AnyElement, App, IntoElement, ParentElement, SharedString, Styled, px};
|
||||
use theme::ActiveTheme;
|
||||
use ui::avatar::{Avatar, PixelAvatar};
|
||||
use ui::{Icon, IconName, Sizable, h_flex};
|
||||
|
||||
pub(crate) fn nav_avatar(
|
||||
seed: Option<impl Into<SharedString>>,
|
||||
picture: Option<impl Into<ImageSource>>,
|
||||
cx: &App,
|
||||
) -> Option<AnyElement> {
|
||||
if AppSettings::get_hide_avatar(cx) {
|
||||
return None;
|
||||
}
|
||||
|
||||
match (seed.map(Into::into), picture.map(Into::into)) {
|
||||
(None, None) => None,
|
||||
(seed, Some(picture)) => Some(
|
||||
Avatar::from_source(picture)
|
||||
.when_some(seed, |avatar, seed| avatar.seed(seed))
|
||||
.small()
|
||||
.flex_shrink_0()
|
||||
.into_any_element(),
|
||||
),
|
||||
(Some(seed), None) => Some(
|
||||
PixelAvatar::new(seed)
|
||||
.small()
|
||||
.flex_shrink_0()
|
||||
.into_any_element(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn nav_icon(icon: IconName, cx: &App) -> AnyElement {
|
||||
h_flex()
|
||||
.flex_shrink_0()
|
||||
|
||||
Reference in New Issue
Block a user