update avatar

This commit is contained in:
2026-09-25 14:56:49 +07:00
parent 192a0585b6
commit a29ce9f827
17 changed files with 94 additions and 72 deletions
@@ -1,21 +1,26 @@
use gpui::prelude::*;
use gpui::{App, SharedString, StyleRefinement, Window};
use gpui_component::avatar::Avatar;
use gpui_base::{Avatar as BaseAvatar, AvatarFallback, AvatarImage};
use gpui_component::{ActiveTheme, Sizable, Size, StyledExt};
/// A small user avatar from gpui-component [`Avatar`], rounded with the theme radius.
/// It shows the user's picture or falls back to name initials.
use crate::pixel_avatar::{PixelAvatar, side_length};
/// A user avatar built on the unstyled [`gpui_base::Avatar`].
///
/// It shows the user's picture when one is set, and falls back to the
/// deterministic pixel avatar seeded from the name otherwise.
#[derive(IntoElement)]
pub struct UserAvatar {
pub struct Avatar {
name: SharedString,
picture: Option<SharedString>,
size: Size,
style: StyleRefinement,
}
impl UserAvatar {
impl Avatar {
/// Create an avatar for `name`.
/// The name seeds the initials fallback shown when no picture is set.
///
/// The name seeds the pixel fallback shown when no picture is set.
pub fn new(name: impl Into<SharedString>) -> Self {
Self {
name: name.into(),
@@ -32,26 +37,35 @@ impl UserAvatar {
}
}
impl Sizable for UserAvatar {
impl Sizable for Avatar {
fn with_size(mut self, size: impl Into<Size>) -> Self {
self.size = size.into();
self
}
}
impl Styled for UserAvatar {
impl Styled for Avatar {
fn style(&mut self) -> &mut StyleRefinement {
&mut self.style
}
}
impl RenderOnce for UserAvatar {
impl RenderOnce for Avatar {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
Avatar::new()
.name(self.name)
.when_some(self.picture, |this, url| this.src(url))
let fallback = AvatarFallback::new()
.size_full()
.child(PixelAvatar::new(self.name.clone()).size_full());
BaseAvatar::new()
.size(side_length(self.size))
.flex_shrink_0()
.rounded(cx.theme().radius)
.overflow_hidden()
.bg(cx.theme().secondary)
.when_some(self.picture, |this, url| {
this.image(AvatarImage::new(url).size_full().rounded(cx.theme().radius))
})
.fallback(fallback)
.refine_style(&self.style)
.with_size(self.size)
}
}
+2 -2
View File
@@ -1,3 +1,4 @@
mod avatar;
mod dropdown_button;
mod nav_item;
mod pixel_avatar;
@@ -8,11 +9,11 @@ mod setting;
mod status_badge;
mod title_bar;
mod tree_row;
mod user_avatar;
pub mod copy_row;
pub mod util;
pub use avatar::Avatar;
pub use copy_row::{copy_row, menu_copy_row};
pub use dropdown_button::DropdownButton;
pub use nav_item::NavItem;
@@ -24,5 +25,4 @@ pub use setting::{SelectOption, setting_block, setting_row};
pub use status_badge::status_badge;
pub use title_bar::title_bar_drag_handlers;
pub use tree_row::tree_row;
pub use user_avatar::UserAvatar;
pub use util::middle_truncate;
+21 -13
View File
@@ -5,7 +5,10 @@ use gpui_component::{ActiveTheme, Colorize, Sizable, Size};
/// Number of rows and columns in the pixel grid.
const GRID_SIZE: usize = 8;
/// Probability that a cell in the left half 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;
@@ -22,12 +25,10 @@ pub struct PixelAvatar {
impl PixelAvatar {
/// Create an avatar seeded from `seed`.
///
/// The seed should be a stable string unique to the entity the avatar represents.
pub fn new(seed: impl AsRef<str>) -> Self {
Self {
seed: fnv1a(seed.as_ref().as_bytes()),
size: Size::XSmall,
size: Size::Small,
style: StyleRefinement::default(),
}
}
@@ -84,6 +85,7 @@ impl RenderOnce for PixelAvatar {
.grid_rows(GRID_SIZE as u16)
.size(side_length(self.size))
.flex_shrink_0()
.rounded(theme.radius)
.overflow_hidden()
.bg(main.opacity(0.16))
.children(cells)
@@ -91,7 +93,8 @@ impl RenderOnce for PixelAvatar {
}
}
fn side_length(size: Size) -> Pixels {
/// The rendered side length of an avatar at `size`, shared with [`Avatar`].
pub(crate) fn side_length(size: Size) -> Pixels {
match size {
Size::XSmall => px(16.),
Size::Small => px(24.),
@@ -106,8 +109,13 @@ fn pattern(seed: u64) -> [u8; GRID_SIZE * GRID_SIZE] {
let mut pattern = [0u8; GRID_SIZE * GRID_SIZE];
let mut filled = 0usize;
for row in 0..GRID_SIZE {
for col in 0..GRID_SIZE / 2 {
// Only the inner rows and the inner left half are candidates; mirroring
// then keeps the art within the same inset, leaving the outer ring empty.
let art_rows = GRID_SIZE - 2 * MARGIN;
let art_columns = GRID_SIZE / 2 - MARGIN;
for row in MARGIN..GRID_SIZE - MARGIN {
for col in MARGIN..GRID_SIZE / 2 {
if rng.chance(FILL_PROBABILITY) {
let accent = rng.chance(ACCENT_PROBABILITY);
set_cell(&mut pattern, row, col, if accent { 2 } else { 1 });
@@ -117,17 +125,17 @@ fn pattern(seed: u64) -> [u8; GRID_SIZE * GRID_SIZE] {
}
if filled < MIN_FILLED {
let half = GRID_SIZE * GRID_SIZE / 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 / (GRID_SIZE / 2);
let col = ix % (GRID_SIZE / 2);
let ix = (start + offset) % total;
let row = MARGIN + ix / art_columns;
let col = MARGIN + ix % art_columns;
if pattern[row * GRID_SIZE + col] == 0 {
set_cell(&mut pattern, row, col, 1);