Compare commits

...

16 Commits

Author SHA1 Message Date
Danilo Leal
647af5a4fc Merge branch 'main' into add-alternate-avatars 2024-12-23 20:04:25 -03:00
Nate Butler
d980de8b56 update avatar 2024-11-07 10:42:46 -05:00
Nate Butler
f29c65ac78 wip: make loading a bool 2024-11-07 10:26:55 -05:00
Danilo Leal
85a9708817 Change name so "Avatar2" is just "Avatar" 2024-11-05 18:55:52 -03:00
Danilo Leal
ddc9d5efa6 Add documentation 2024-11-05 18:03:43 -03:00
Danilo Leal
03c3c8e81d Merge branch 'main' into add-alternate-avatars 2024-11-05 17:30:24 -03:00
Nate Butler
1ec2245e9a Work on avatar preview 2024-11-05 12:09:34 -05:00
Nate Butler
43a68adb0a Let color consider greyscale 2024-11-05 11:40:23 -05:00
Nate Butler
0bcb21400b Use ? for empty initials 2024-11-05 11:15:09 -05:00
Nate Butler
1f5ace0c68 update avatar rendering 2024-11-05 11:10:10 -05:00
Danilo Leal
3707963546 Change approach to use the AvatarSource enum
Co-Authored-By: Nate Butler <1714999+iamnbutler@users.noreply.github.com>
2024-11-04 23:51:41 -03:00
Danilo Leal
cef48613df Expose each display option as render functions
Co-Authored-By: Nate Butler <1714999+iamnbutler@users.noreply.github.com>
2024-11-04 23:13:00 -03:00
Danilo Leal
8a4e452902 Apply the new avatar component in places the old was used
Co-Authored-By: Nate Butler <1714999+iamnbutler@users.noreply.github.com>
2024-11-04 19:56:09 -03:00
Danilo Leal
101bb23945 Checkpoint: Structure colors and icon index
Co-Authored-By: Nate Butler <1714999+iamnbutler@users.noreply.github.com>
2024-11-04 19:29:58 -03:00
Danilo Leal
ab920e7f14 Set up content depending on avatar type
Co-Authored-By: Nate Butler <1714999+iamnbutler@users.noreply.github.com>
2024-11-04 18:26:40 -03:00
Nate Butler
8da62f3753 wip 2024-11-04 14:35:05 -05:00
13 changed files with 613 additions and 26 deletions

View File

@@ -0,0 +1,3 @@
<svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.23309 0.527832C4.36482 0.527832 1.23395 3.27718 1.23395 7.79257C1.23395 11.2828 5.42719 14.8073 7.32383 16.2259C7.86283 16.6281 8.60492 16.6281 9.14392 16.2259C11.039 14.7789 15.2322 11.2825 15.2322 7.79226C15.2322 3.27687 12.1014 0.527832 8.23309 0.527832ZM6.73328 10.4982H5.7334C4.35482 10.4982 3.2337 9.37738 3.2337 8.0266C3.2337 7.74914 3.45633 7.55479 3.73364 7.55479H4.73352C6.1121 7.55479 7.23322 8.67559 7.23322 10.0542C7.23322 10.3045 7.01137 10.4982 6.73328 10.4982ZM10.7328 10.4982H9.73291C9.4556 10.4982 9.23297 10.2755 9.23297 10.0264C9.23297 8.64778 10.3541 7.52698 11.7327 7.52698H12.7325C13.0099 7.52698 13.2325 7.74945 13.2325 7.9988C13.2325 9.40456 12.1107 10.4982 10.7328 10.4982Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 831 B

View File

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.08125 5C8.45 5 5.74375 5.0625 4 7.6875V5C4 3.34375 2.65625 2 1 2C0.449375 2 0 2.45 0 3C0 3.55 0.449375 4 1 4C1.55063 4 2 4.44937 2 5V13C2 14.1016 2.89844 15 4 15H9.5C9.77734 15 10 14.7773 10 14.5281V14C10 13.4494 9.55063 13 9 13H8L12 10V14.5C12 14.7773 12.2227 15 12.5 15H13.5C13.7773 15 14 14.7773 14 14.5V8.05937C13.6797 8.14141 13.3475 8.2 13 8.2C11.0688 8.2 9.45313 6.825 9.08125 5V5ZM14 2H12L10 0V4.2C10 5.85625 11.3438 7.17188 13 7.17188C14.6562 7.17188 16 5.85625 16 4.2V0L14 2ZM11.75 4.5C11.4727 4.5 11.25 4.27734 11.25 4C11.25 3.72266 11.4727 3.5 11.75 3.5C12.0273 3.5 12.25 3.72266 12.25 4C12.25 4.27734 12.0281 4.5 11.75 4.5ZM14.25 4.5C13.9727 4.5 13.75 4.27734 13.75 4C13.75 3.72266 13.9727 3.5 14.25 3.5C14.5273 3.5 14.75 3.72266 14.75 4C14.75 4.27734 14.5281 4.5 14.25 4.5Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 919 B

View File

@@ -0,0 +1,10 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_2940_542)">
<path d="M13.0472 7.86798C13.1784 8.06796 13.2971 8.28044 13.3909 8.5023H14.382L15.2763 8.05515C15.5241 7.93235 15.8231 8.03109 15.9472 8.27857C16.07 8.52542 15.9706 8.82601 15.7235 8.94944L14.7236 9.44939C14.6529 9.48314 14.578 9.50188 14.4998 9.50188H13.654C13.6827 9.69843 13.714 9.89497 13.714 10.1C13.714 10.1909 13.6827 10.2752 13.6618 10.3624L14.158 10.5277C14.2311 10.5521 14.2986 10.5933 14.3533 10.648L15.3532 11.6479C15.5485 11.8432 15.5485 12.1598 15.3532 12.3547C15.2557 12.4513 15.1276 12.5013 14.9995 12.5013C14.8714 12.5013 14.7436 12.4522 14.6461 12.3547L13.7293 11.4383L13.0687 11.218L12.1091 12.1776L13.1578 12.5272C13.2309 12.5516 13.2984 12.5928 13.3531 12.6475L14.353 13.6474C14.5483 13.8427 14.5483 14.1592 14.353 14.3542C14.2558 14.4505 14.1277 14.5005 13.9996 14.5005C13.8715 14.5005 13.7437 14.4514 13.6462 14.3539L12.7294 13.4374L11.3192 12.9672L10.793 13.4934L11.757 14.0718C11.9091 14.1627 11.9998 14.3252 11.9998 14.5001V15.5C11.9998 15.7763 11.776 16 11.4998 16C11.2233 16 10.9999 15.775 10.9999 15.5V14.7832L9.69374 14.0002H6.30657L5.00045 14.7845V15.5C5.00045 15.775 4.77547 16 4.5005 16C4.22552 16 4.00055 15.775 4.00055 15.5V14.5001C4.00055 14.3244 4.0921 14.1617 4.24365 14.0714L5.20762 13.4931L4.68142 12.9669L3.27093 13.4371L2.35415 14.3536C2.25634 14.4502 2.12823 14.5001 2.00043 14.5001C1.87263 14.5001 1.74452 14.4511 1.64703 14.3536C1.45173 14.1583 1.45173 13.8418 1.64703 13.6468L2.64693 12.6469C2.70161 12.5922 2.76879 12.5506 2.84222 12.5263L3.89087 12.1766L2.93128 11.217L2.27072 11.437L1.35393 12.3535C1.25644 12.4497 1.12833 12.4997 1.00053 12.4997C0.872727 12.4997 0.744615 12.4507 0.647124 12.3532C0.451831 12.1579 0.451831 11.8413 0.647124 11.6464L1.64703 10.6465C1.70171 10.5918 1.76889 10.5502 1.84232 10.5258L2.33852 10.3602C2.31727 10.2731 2.28696 10.1887 2.28696 10.0978C2.28696 9.89278 2.31759 9.69655 2.34633 9.4997H1.50017C1.42299 9.4997 1.34581 9.48157 1.27644 9.44689L0.276536 8.94694C0.0293721 8.82445 -0.0703056 8.52448 0.0528073 8.2745C0.176545 8.02672 0.476516 7.92923 0.723679 8.05078L1.61797 8.49917H2.60912C2.70349 8.27575 2.82222 8.06609 2.95346 7.86517L0.430895 6.67747C0.167796 6.55561 0 6.29001 0 5.99941V3.99961C0 1.79076 1.79045 0 3.97149 0H5.94317L3.97149 1.9998H5.94317C5.94317 3.10438 5.04794 3.99961 3.97149 3.99961H1.49985V5.52352L4.05804 6.72747C4.61799 6.34001 5.28355 6.10253 5.9991 6.03691V4.49956C5.9991 4.22459 6.22408 3.99961 6.49905 3.99961C6.77403 3.99961 6.999 4.22459 6.999 4.49956L7.00213 5.99941H8.99881V4.49956C8.99881 4.22459 9.22379 3.99961 9.49876 3.99961C9.77373 3.99961 9.99871 4.22459 9.99871 4.49956L9.9984 6.03597C10.7136 6.10347 11.3792 6.33969 11.9398 6.72747L14.498 5.52352V3.99961H12.0263C10.8952 3.99961 9.97028 3.10438 9.97028 1.9998H11.942L9.97028 0H11.942C14.1508 0 15.9134 1.79076 15.9134 3.99961V5.99941C15.9134 6.2897 15.7455 6.55405 15.4829 6.6781L13.045 7.86486L13.0472 7.86798Z" fill="black"/>
</g>
<defs>
<clipPath id="clip0_2940_542">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16 4.67221C16 5.28485 15.5033 5.78157 14.8906 5.78157C14.8837 5.78157 14.8779 5.77796 14.8709 5.77768L13.4698 13.485C13.3927 13.9046 13.0266 14.2124 12.5968 14.2124H3.42792C2.99915 14.2124 2.6314 13.9057 2.55485 13.4838L1.15401 5.77851C1.14708 5.77851 1.14125 5.78129 1.10936 5.78129C0.496715 5.78129 0 5.28457 0 4.67193C0 4.05929 0.521676 3.56257 1.10936 3.56257C1.69704 3.56257 2.21872 4.05929 2.21872 4.67193C2.21872 4.92126 2.12082 5.14036 1.98187 5.32562L4.46766 7.31414C4.90891 7.6672 5.56316 7.52326 5.81581 7.01795L7.41329 3.82299C7.09962 3.62747 6.87775 3.29466 6.87775 2.89696C6.87775 2.28431 7.39914 1.7876 8.01206 1.7876C8.62499 1.7876 9.09646 2.28431 9.09646 2.89696C9.09646 3.29466 8.87542 3.62747 8.5612 3.82327L10.1587 7.01822C10.4113 7.52354 11.0659 7.6672 11.5068 7.31442L13.9926 5.3259C13.8778 5.14063 13.7807 4.89658 13.7807 4.67193C13.7807 4.05901 14.2772 3.56257 14.8901 3.56257C15.503 3.56257 15.9994 4.05901 15.9994 4.67193L16 4.67221Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0.483129 8.3934L4.82129 7.5986L2.54186 9.31317C2.28868 9.5681 2.46688 9.99799 2.82179 9.99799H8.3879C8.15046 9.33566 8.01924 8.63584 8.01924 7.91402V6.85378L5.02874 4.66933C4.43389 4.27143 3.64659 4.31817 3.10422 4.7793L0.167458 7.68857C-0.141714 7.94851 0.0832291 8.44838 0.483129 8.3934ZM14.3989 9.24568L11.882 7.98975C11.4759 7.78667 11.2197 7.37115 11.2197 6.91502V5.999H12.819L13.5219 6.56435C13.6716 6.71382 13.874 6.7988 14.0865 6.7988H14.8388C15.1162 6.7988 15.4166 6.62696 15.5511 6.35516L15.9103 5.63984C16.0445 5.36816 16.0165 5.04624 15.8353 4.80255L13.9735 2.31892C13.8213 2.11872 13.6063 2 13.3564 2H7.42039C7.24293 2 7.15546 2.21557 7.28042 2.34041L8.79754 3.5996L7.33041 4.21819C7.18345 4.29317 7.18345 4.50237 7.33041 4.57735L8.79754 5.1992L8.79729 7.91352C8.79729 9.71307 9.69682 11.3976 11.1964 12.3974C6.30767 12.5661 2.5886 13.4221 0.346663 13.919C0.143464 13.9627 0 14.144 0 14.3534C0.0229942 14.5968 0.222944 14.7968 0.466633 14.7968H12.941C14.5216 14.7968 15.9303 13.6096 15.9927 12.0275C16.0755 10.8478 15.4456 9.77055 14.3984 9.24568H14.3989ZM12.2469 3.65583L13.3901 3.94026C13.3214 4.21519 13.0777 4.41214 12.7873 4.3964C12.4619 4.38065 12.1395 4.08398 12.2469 3.65583Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.81568 0.00306437C4.53127 0.100565 2 2.96403 2 6.24999V14.5001C2 14.9454 2.54063 15.1682 2.85313 14.8516L3.63283 14.2735C3.83986 14.1169 4.13283 14.1482 4.30471 14.3438L5.64847 15.8535C5.84378 16.0488 6.16004 16.0488 6.35535 15.8535L7.62474 14.4219C7.82396 14.1951 8.17537 14.1951 8.37474 14.4219L9.64444 15.8535C9.83976 16.0488 10.1563 16.0488 10.3513 15.8535L11.6951 14.3438C11.867 14.1482 12.1601 14.1169 12.367 14.2735L13.1467 14.8516C13.4592 15.1682 13.9998 14.9454 13.9998 14.5001V5.99968C14.0001 2.62403 11.2157 -0.0988115 7.81568 0.00275186V0.00306437ZM6.00003 6.99969C5.4494 6.99969 5.02815 6.55031 5.02815 5.99937C5.02815 5.44842 5.47753 4.99905 6.00003 4.99905C6.52254 4.99905 7.00004 5.44842 7.00004 5.99937C7.00004 6.55031 6.55004 6.99969 6.00003 6.99969ZM10.0001 6.99969C9.44944 6.99969 9.00006 6.55031 9.00006 5.99937C9.00006 5.44842 9.44944 4.99905 10.0001 4.99905C10.5507 4.99905 11.0001 5.44842 11.0001 5.99937C11.0001 6.55031 10.5501 6.99969 10.0001 6.99969Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.5556 4.66722H14.6667C14.4201 4.66722 14.2222 4.86514 14.2222 5.08667V7.30889H13.3333V6.00028C13.3333 5.75375 13.1354 5.55583 12.8889 5.55583H11.5556V3.77806H12.8889C13.1361 3.77806 13.3333 3.58028 13.3333 3.33361V2.44472C13.3333 2.19806 13.1361 2.00028 12.8889 2.00028H12C11.7528 2.00028 11.5556 2.19806 11.5556 2.44472V2.88917H10.2222C9.97569 2.88917 9.77778 3.08708 9.77778 3.33361V4.66694H6.22222V3.33361C6.22222 3.08708 6.02431 2.88917 5.77778 2.88917L4.44444 2.88889V2.44444C4.44444 2.19778 4.24722 2 4 2H3.11111C2.86389 2 2.66667 2.19778 2.66667 2.44444V3.33333C2.66667 3.58 2.86389 3.77778 3.11111 3.77778H4.44444V5.55556H3.11111C2.86458 5.55556 2.66667 5.75347 2.66667 6V7.33333H1.77778V5.11111C1.77778 4.86458 1.57986 4.69167 1.33333 4.69167L0.444444 4.69139C0.197778 4.69139 0 4.88931 0 5.11083V8.66639C0 8.91292 0.197778 9.11083 0.444444 9.11083H1.77778V11.3331C1.77778 11.5796 1.97569 11.7775 2.22222 11.7775H3.55556V13.9997C3.55556 14.2462 3.75347 14.4442 4 14.4442H6.66667C6.91319 14.4442 7.11111 14.2462 7.11111 14.0247V13.1608C7.11111 12.9143 6.91319 12.7164 6.66667 12.7164H5.33333V11.8275H10.6667V12.7164H9.33333C9.08681 12.7164 8.88889 12.9143 8.88889 13.1608V14.0247C8.88889 14.2712 9.08681 14.4442 9.33333 14.4442H12C12.2465 14.4442 12.4444 14.2462 12.4444 13.9997V11.7775H13.7778C14.0243 11.7775 14.2222 11.5796 14.2222 11.3331V9.11083H15.5556C15.8021 9.11083 16 8.91292 16 8.66639V5.11083C16 4.86361 15.8028 4.66639 15.5556 4.66639V4.66722ZM6.22222 10.0006H4.44444V7.33389H6.22222V10.0006ZM11.5556 10.0006H9.77778V7.33389H11.5556V10.0006Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -222,6 +222,7 @@ impl TitleBar {
Facepile::empty()
.child(
Avatar::new(user.avatar_uri.clone())
.fallback_initials(user.github_login.clone().to_string())
.grayscale(!is_present)
.border_color(if is_speaking {
cx.theme().status().info

View File

@@ -1,7 +1,9 @@
mod avatar;
mod avatar_audio_status_indicator;
mod avatar_availability_indicator;
mod avatar_old;
pub use avatar::*;
pub use avatar_audio_status_indicator::*;
pub use avatar_availability_indicator::*;
pub use avatar_old::*;

View File

@@ -1,49 +1,216 @@
use crate::prelude::*;
use std::time::Duration;
use gpui::{img, AnyElement, Hsla, ImageSource, Img, IntoElement, Styled};
use crate::prelude::*;
use gpui::{
hsla, img, pulsating_between, AbsoluteLength, Animation, AnimationExt, AnyElement, FontWeight,
Hsla, ImageSource, IntoElement, SharedString,
};
use strum::IntoEnumIterator;
const DEFAULT_AVATAR_SIZE: f32 = 20.0;
/// A collection of types of content that can be used for the avatar
#[derive(Debug, Clone, PartialEq)]
pub enum AvatarSource {
/// The avatar's content is an image
Image(ImageSource),
/// The avatar's content is an icon
Icon(AnonymousAvatarIcon),
}
/// A collection of fallback strategies when the primary source fails
#[derive(Debug, Clone, PartialEq)]
pub enum AvatarFallback {
/// Use initials as fallback
Initials(SharedString),
/// Use an anonymous icon as fallback
Anonymous(usize),
}
/// A collection of random icons to be used as the anonymous avatars content
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, strum::EnumIter)]
pub enum AnonymousAvatarIcon {
/// A crown icon
Crown,
/// A cat icon
Cat,
/// A dragon icon
Dragon,
/// An alian icon
Alien,
/// A ghost icon
Ghost,
/// A crab icon
#[default]
Crab,
/// An alternative alien icon
Invader,
}
impl Into<IconName> for AnonymousAvatarIcon {
fn into(self) -> IconName {
match self {
AnonymousAvatarIcon::Crown => IconName::AnonymousCrown,
AnonymousAvatarIcon::Cat => IconName::AnonymousCat,
AnonymousAvatarIcon::Dragon => IconName::AnonymousDragon,
AnonymousAvatarIcon::Alien => IconName::AnonymousAlien,
AnonymousAvatarIcon::Ghost => IconName::AnonymousGhost,
AnonymousAvatarIcon::Crab => IconName::AnonymousCrab,
AnonymousAvatarIcon::Invader => IconName::AnonymousInvader,
}
}
}
impl TryFrom<IconName> for AnonymousAvatarIcon {
type Error = String;
fn try_from(icon: IconName) -> Result<Self, Self::Error> {
match icon {
IconName::AnonymousCrown => Ok(AnonymousAvatarIcon::Crown),
IconName::AnonymousCat => Ok(AnonymousAvatarIcon::Cat),
IconName::AnonymousDragon => Ok(AnonymousAvatarIcon::Dragon),
IconName::AnonymousAlien => Ok(AnonymousAvatarIcon::Alien),
IconName::AnonymousGhost => Ok(AnonymousAvatarIcon::Ghost),
IconName::AnonymousCrab => Ok(AnonymousAvatarIcon::Crab),
IconName::AnonymousInvader => Ok(AnonymousAvatarIcon::Invader),
_ => Err("Icon can't be turned into an AnonymousAvatarIcon.".to_string()),
}
}
}
impl AnonymousAvatarIcon {
/// Returns an anonymous avatar icon based on the provided index.
pub fn from_index(index: usize) -> Self {
let variants = Self::iter().collect::<Vec<_>>();
variants[index % variants.len()]
}
}
/// An element that renders a user avatar with customizable appearance options.
///
/// # Examples
///
/// ```
/// use ui::{Avatar, AvatarShape};
/// use ui::Avatar;
///
/// Avatar::new("path/to/image.png")
/// .shape(AvatarShape::Circle)
/// .grayscale(true)
/// .border_color(gpui::red());
/// ```
#[derive(IntoElement)]
pub struct Avatar {
image: Img,
source: Option<AvatarSource>,
fallback: Option<AvatarFallback>,
size: Option<AbsoluteLength>,
border_color: Option<Hsla>,
indicator: Option<AnyElement>,
grayscale: bool,
loading: bool,
player_index: Option<usize>,
}
impl Avatar {
/// Creates a new avatar element with the specified image source.
pub fn new(src: impl Into<ImageSource>) -> Self {
/// Creates a new avatar with just a fallback defined, no source
pub fn empty() -> Self {
Avatar {
image: img(src),
source: None,
fallback: None,
size: None,
border_color: None,
indicator: None,
grayscale: false,
loading: false,
player_index: None,
}
}
/// Creates a new avatar with an optional image source
pub fn new(src: impl Into<ImageSource>) -> Self {
Self::empty().source(Some(AvatarSource::Image(src.into())))
}
/// Creates a new avatar with an anonymous icon
pub fn new_anonymous(player_index: impl Into<Option<usize>>) -> Self {
let player_index = player_index.into();
let icon = match player_index {
Some(index) => AnonymousAvatarIcon::from_index(index),
None => AnonymousAvatarIcon::default(),
};
Self::empty()
.source(Some(AvatarSource::Icon(icon)))
.player_index(player_index.unwrap_or_default())
}
/// Sets the source of the avatar
pub fn source(mut self, source: Option<AvatarSource>) -> Self {
self.source = source;
self
}
/// Sets the player index for the avatar
pub fn player_index(mut self, index: usize) -> Self {
self.player_index = Some(index);
self
}
/// Uses the user name's first letter as a fallback if their avatar image
/// fails to load
///
/// # Examples
///
/// ```
/// use ui::Avatar;
///
/// div().children(
/// PLAYER_HANDLES.iter().enumerate().map(|(ix, handle)| {
/// Avatar::new_fallback()
/// .fallback_initials(handle.to_string())
/// .fallback_anonymous(ix as u32)
/// }),
///
/// ```
pub fn fallback_initials(mut self, initials: impl Into<SharedString>) -> Self {
let initials = initials.into();
self.fallback = Some(AvatarFallback::Initials(if initials.is_empty() {
"?".into()
} else {
initials
}));
self
}
/// Sets anonymous icon as a fallback
pub fn fallback_anonymous(mut self, index: usize) -> Self {
self.fallback = Some(AvatarFallback::Anonymous(index));
self
}
/// Uses a pulsating background animation to indicate the loading state
///
/// # Examples
///
/// ```
/// use ui::Avatar;
///
/// let avatar = Avatar::new("path/to/image.png").loading(true);
/// ```
pub fn loading(mut self, loading: bool) -> Self {
self.loading = loading;
self
}
/// Applies a grayscale filter to the avatar image.
///
/// # Examples
///
/// ```
/// use ui::{Avatar, AvatarShape};
/// use ui::Avatar;
///
/// let avatar = Avatar::new("path/to/image.png").grayscale(true);
/// ```
pub fn grayscale(mut self, grayscale: bool) -> Self {
self.image = self.image.grayscale(grayscale);
self.grayscale = grayscale;
self
}
@@ -68,31 +235,180 @@ impl Avatar {
self.indicator = indicator.into().map(IntoElement::into_any_element);
self
}
fn base_avatar_style(&self, size: Pixels) -> Div {
div()
.size(size)
.rounded_full()
.overflow_hidden()
.flex()
.items_center()
.justify_center()
}
fn color(&self, cx: &WindowContext) -> Hsla {
if self.grayscale {
return hsla(0.0, 0.0, 0.5, 1.0);
}
if let Some(player_index) = self.player_index {
return cx
.theme()
.players()
.color_for_participant(player_index as u32)
.cursor;
}
match &self.source {
Some(AvatarSource::Icon(icon)) => {
cx.theme()
.players()
.color_for_participant((*icon as u8).into())
.cursor
}
None => match &self.fallback {
Some(AvatarFallback::Initials(initials)) => {
let index = initials.chars().next().map(|c| c as u8).unwrap_or(0);
cx.theme()
.players()
.color_for_participant(index.into())
.cursor
}
Some(AvatarFallback::Anonymous(index)) => {
cx.theme()
.players()
.color_for_participant(*index as u32)
.cursor
}
None => cx.theme().colors().text,
},
_ => cx.theme().colors().text,
}
}
fn render_content(&self, content_size: Pixels, cx: &WindowContext) -> AnyElement {
if self.loading {
return self.render_loading_avatar(content_size, cx);
}
match &self.source {
Some(AvatarSource::Image(image)) => self.render_image(image, content_size),
Some(AvatarSource::Icon(icon)) => self.render_anonymous_avatar(*icon, content_size, cx),
None => match &self.fallback {
Some(AvatarFallback::Initials(initials)) => {
self.render_fallback_avatar(initials, content_size, cx)
}
Some(AvatarFallback::Anonymous(index)) => self.render_anonymous_avatar(
AnonymousAvatarIcon::from_index(*index),
content_size,
cx,
),
None => self.render_fallback_avatar("?", content_size, cx),
},
}
}
fn render_image(&self, image: &ImageSource, content_size: Pixels) -> AnyElement {
self.base_avatar_style(content_size)
.child(
img(image.clone())
.size(content_size)
.rounded_full()
.when(self.grayscale, |img| img.grayscale(true)),
)
.into_any_element()
}
fn render_anonymous_avatar(
&self,
icon: AnonymousAvatarIcon,
content_size: Pixels,
cx: &WindowContext,
) -> AnyElement {
let color = self.color(cx);
let bg_color = color.opacity(0.2);
self.base_avatar_style(content_size)
.bg(bg_color)
.child(
Icon::new(icon.into())
.size(IconSize::Indicator)
.color(Color::Custom(color)),
)
.into_any_element()
}
fn render_fallback_avatar(
&self,
initials: &str,
content_size: Pixels,
cx: &WindowContext,
) -> AnyElement {
let color = self.color(cx);
let bg_color = color.opacity(0.2);
let first_letter = initials
.chars()
.next()
.unwrap_or('?')
.to_string()
.to_uppercase();
self.base_avatar_style(content_size)
.bg(bg_color)
.child(
div()
.size_full()
.flex()
.items_center()
.justify_center()
.text_color(color)
.text_size(px(10.))
.line_height(relative(1.))
.font_weight(FontWeight::BOLD)
.child(first_letter),
)
.into_any_element()
}
fn render_loading_avatar(&self, content_size: Pixels, cx: &WindowContext) -> AnyElement {
let color = self.color(cx);
self.base_avatar_style(content_size)
.bg(cx.theme().colors().element_background)
.with_animation(
"pulsating-bg",
Animation::new(Duration::from_secs(2))
.repeat()
.with_easing(pulsating_between(0.3, 0.7)),
move |this, delta| this.bg(color.opacity(0.8 - delta)),
)
.into_any_element()
}
}
impl RenderOnce for Avatar {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
let rem_size = cx.rem_size();
let base_size = self.size.unwrap_or_else(|| px(DEFAULT_AVATAR_SIZE).into());
let content_size = base_size.to_pixels(rem_size);
let border_width = if self.border_color.is_some() {
px(2.)
px(2.0)
} else {
px(0.)
px(0.0)
};
let image_size = self.size.unwrap_or_else(|| rems(1.).into());
let container_size = image_size.to_pixels(cx.rem_size()) + border_width * 2.;
let container_size = content_size + (border_width * 2.0);
div()
.id("avatar")
.size(container_size)
.rounded_full()
.when_some(self.border_color, |this, color| {
this.border(border_width).border_color(color)
})
.child(
self.image
.size(image_size)
.rounded_full()
.bg(cx.theme().colors().ghost_element_background),
)
.children(self.indicator.map(|indicator| div().child(indicator)))
.child(self.render_content(content_size, cx))
.when_some(self.indicator, |this, indicator| {
this.child(div().absolute().bottom_0().right_0().child(indicator))
})
}
}

View File

@@ -0,0 +1,98 @@
use crate::prelude::*;
use gpui::{img, AnyElement, Hsla, ImageSource, Img, IntoElement, Styled};
/// An element that renders a user avatar with customizable appearance options.
///
/// # Examples
///
/// ```
/// use ui::{Avatar, AvatarShape};
///
/// Avatar::new("path/to/image.png")
/// .shape(AvatarShape::Circle)
/// .grayscale(true)
/// .border_color(gpui::red());
/// ```
#[derive(IntoElement)]
pub struct AvatarOld {
image: Img,
size: Option<AbsoluteLength>,
border_color: Option<Hsla>,
indicator: Option<AnyElement>,
}
impl AvatarOld {
/// Creates a new avatar element with the specified image source.
pub fn new(src: impl Into<ImageSource>) -> Self {
AvatarOld {
image: img(src),
size: None,
border_color: None,
indicator: None,
}
}
/// Applies a grayscale filter to the avatar image.
///
/// # Examples
///
/// ```
/// use ui::{Avatar, AvatarShape};
///
/// let avatar = Avatar::new("path/to/image.png").grayscale(true);
/// ```
pub fn grayscale(mut self, grayscale: bool) -> Self {
self.image = self.image.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
}
/// Size overrides the avatar size. By default they are 1rem.
pub fn size<L: Into<AbsoluteLength>>(mut self, size: impl Into<Option<L>>) -> Self {
self.size = size.into().map(Into::into);
self
}
/// Sets the current indicator to be displayed on the avatar, if any.
pub fn indicator<E: IntoElement>(mut self, indicator: impl Into<Option<E>>) -> Self {
self.indicator = indicator.into().map(IntoElement::into_any_element);
self
}
}
impl RenderOnce for AvatarOld {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
let border_width = if self.border_color.is_some() {
px(2.)
} else {
px(0.)
};
let image_size = self.size.unwrap_or_else(|| rems(1.).into());
let container_size = image_size.to_pixels(cx.rem_size()) + border_width * 2.;
div()
.size(container_size)
.rounded_full()
.when_some(self.border_color, |this, color| {
this.border(border_width).border_color(color)
})
.child(
self.image
.size(image_size)
.rounded_full()
.bg(cx.theme().colors().ghost_element_background),
)
.children(self.indicator.map(|indicator| div().child(indicator)))
}
}

View File

@@ -112,6 +112,13 @@ impl IconSize {
#[strum(serialize_all = "snake_case")]
#[path_str(prefix = "icons", suffix = ".svg")]
pub enum IconName {
AnonymousCrown,
AnonymousCat,
AnonymousDragon,
AnonymousAlien,
AnonymousGhost,
AnonymousCrab,
AnonymousInvader,
Ai,
AiAnthropic,
AiAnthropicHosted,

View File

@@ -3,10 +3,10 @@ use gpui::{actions, hsla, AnyElement, AppContext, EventEmitter, FocusHandle, Foc
use strum::IntoEnumIterator;
use theme::all_theme_colors;
use ui::{
element_cell, prelude::*, string_cell, utils::calculate_contrast_ratio, AudioStatus,
Availability, Avatar, AvatarAudioStatusIndicator, AvatarAvailabilityIndicator, ButtonLike,
Checkbox, CheckboxWithLabel, ContentGroup, DecoratedIcon, ElevationIndex, Facepile,
IconDecoration, Indicator, Switch, SwitchWithLabel, Table, TintColor, Tooltip,
prelude::*, utils::calculate_contrast_ratio, AudioStatus, Availability, Avatar,
AvatarAudioStatusIndicator, AvatarAvailabilityIndicator, AvatarOld, ButtonLike, Checkbox,
CheckboxWithLabel, ContentGroup, DecoratedIcon, element_cell, ElevationIndex, Facepile,
IconDecoration, Indicator, string_cell, Switch, SwitchWithLabel, Table, TintColor, Tooltip,
};
use crate::{Item, Workspace};
@@ -102,12 +102,147 @@ impl Item for ThemePreview {
}
const AVATAR_URL: &str = "https://avatars.githubusercontent.com/u/1714999?v=4";
const PLAYER_HANDLES: [&str; 4] = ["iamnbutler", "Danilo Leal", "zed-fan-89", ""];
impl ThemePreview {
fn preview_bg(cx: &WindowContext) -> Hsla {
cx.theme().colors().editor_background
}
fn render_avatars(&self, cx: &ViewContext<Self>) -> impl IntoElement {
let avatar_url = SharedString::from(AVATAR_URL);
v_flex()
.gap_1()
.child(
Headline::new("Avatar")
.size(HeadlineSize::Small)
.color(Color::Muted),
)
.child(
v_flex().items_start().gap_4().child(
h_flex()
.items_start()
.gap_3()
.child(
v_flex()
.gap_1()
.child(Label::new("Default").color(Color::Muted))
.child(Avatar::new(avatar_url.clone()))
.child(Label::new("Default, Grayscale").color(Color::Muted))
.child(Avatar::new(avatar_url.clone()).grayscale(true)),
)
.child(
v_flex()
.gap_1()
.child(Label::new("Anonymous").color(Color::Muted))
.child(
h_flex()
.gap_1()
.children((0..=5).map(|ix| Avatar::new_anonymous(ix))),
)
.child(Label::new("Anonymous, Grayscale").color(Color::Muted))
.child(h_flex().gap_1().children(
(0..=5).map(|ix| Avatar::new_anonymous(ix).grayscale(true)),
)),
)
.child(
v_flex()
.gap_1()
.child(Label::new("Initials").color(Color::Muted))
.child(h_flex().gap_1().children(PLAYER_HANDLES.iter().map(
|handle| Avatar::empty().fallback_initials(handle.to_string()),
)))
.child(Label::new("Initials, Grayscale").color(Color::Muted))
.child(h_flex().gap_1().children(
PLAYER_HANDLES.iter().enumerate().map(|(ix, handle)| {
Avatar::empty()
.fallback_initials(handle.to_string())
.grayscale(true)
}),
)),
)
.child(
v_flex()
.gap_1()
.child(Label::new("Indicators").color(Color::Muted))
.child(
h_flex()
.gap_2()
.child(Avatar::new(avatar_url.clone()).indicator(
AvatarAudioStatusIndicator::new(AudioStatus::Deafened),
))
.child(Avatar::new(avatar_url.clone()).indicator(
AvatarAvailabilityIndicator::new(Availability::Free),
)),
)
.child(Label::new("Loading").color(Color::Muted))
.child(Avatar::new(avatar_url.clone()).loading(true)),
),
),
)
.child(
Headline::new("Old Avatars")
.size(HeadlineSize::Small)
.color(Color::Muted),
)
.child(
h_flex()
.items_start()
.gap_4()
.child(AvatarOld::new(AVATAR_URL).size(px(24.)))
.child(AvatarOld::new(AVATAR_URL).size(px(24.)).grayscale(true))
.child(
AvatarOld::new(AVATAR_URL)
.size(px(24.))
.indicator(AvatarAudioStatusIndicator::new(AudioStatus::Muted)),
)
.child(
AvatarOld::new(AVATAR_URL)
.size(px(24.))
.indicator(AvatarAudioStatusIndicator::new(AudioStatus::Deafened)),
)
.child(
AvatarOld::new(AVATAR_URL)
.size(px(24.))
.indicator(AvatarAvailabilityIndicator::new(Availability::Free)),
)
.child(
AvatarOld::new(AVATAR_URL)
.size(px(24.))
.indicator(AvatarAvailabilityIndicator::new(Availability::Free)),
)
.child(
Facepile::empty()
.child(
AvatarOld::new(AVATAR_URL)
.border_color(Self::preview_bg(cx))
.size(px(22.))
.into_any_element(),
)
.child(
AvatarOld::new(AVATAR_URL)
.border_color(Self::preview_bg(cx))
.size(px(22.))
.into_any_element(),
)
.child(
AvatarOld::new(AVATAR_URL)
.border_color(Self::preview_bg(cx))
.size(px(22.))
.into_any_element(),
)
.child(
AvatarOld::new(AVATAR_URL)
.border_color(Self::preview_bg(cx))
.size(px(22.))
.into_any_element(),
),
),
)
}
fn render_text(&self, layer: ElevationIndex, cx: &ViewContext<Self>) -> impl IntoElement {
let bg = layer.bg(cx);