forked from reya/gpui-native-kit
320 lines
9.2 KiB
Rust
320 lines
9.2 KiB
Rust
use std::fmt::Display;
|
|
|
|
use gpui::prelude::FluentBuilder as _;
|
|
use gpui::{
|
|
App, ElementId, InteractiveElement as _, IntoElement, ParentElement as _, RenderOnce, Role,
|
|
SharedString, StatefulInteractiveElement as _, StyleRefinement, Styled as _, Window, div,
|
|
};
|
|
use gpui_native_theme::{ActiveTheme as _, Oklch, Size, ThemeVariant};
|
|
|
|
use crate::common::{Sizable, StyledExt, h_flex};
|
|
|
|
/// Number of rows and columns in the pixel grid.
|
|
const GRID_SIZE: usize = 8;
|
|
/// Empty cells kept between the pattern and the avatar edge.
|
|
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 shade instead of the main color.
|
|
const SHADE_PROBABILITY: f32 = 0.25;
|
|
/// Minimum number of filled left-half cells.
|
|
const MIN_FILLED: usize = 5;
|
|
|
|
/// An avatar for a user or other entity.
|
|
#[derive(IntoElement)]
|
|
pub struct Avatar {
|
|
id: SharedString,
|
|
name: Option<SharedString>,
|
|
picture: Option<SharedString>,
|
|
size: Size,
|
|
style: StyleRefinement,
|
|
}
|
|
|
|
impl Avatar {
|
|
/// Create an avatar for `id`, a public key or any other stable string.
|
|
///
|
|
/// The id seeds the pixel fallback shown when no picture is set.
|
|
pub fn new<T>(id: T) -> Self
|
|
where
|
|
T: Display,
|
|
{
|
|
Self {
|
|
id: id.to_string().into(),
|
|
name: None,
|
|
picture: None,
|
|
size: Size::default(),
|
|
style: StyleRefinement::default(),
|
|
}
|
|
}
|
|
|
|
/// The display name of the avatar's subject, if known.
|
|
pub fn name<N>(mut self, name: N) -> Self
|
|
where
|
|
N: Into<SharedString>,
|
|
{
|
|
self.name = Some(name.into());
|
|
self
|
|
}
|
|
|
|
/// The user's picture URL, if known. `None` clears it.
|
|
pub fn picture<P>(mut self, picture: Option<P>) -> Self
|
|
where
|
|
P: Into<SharedString>,
|
|
{
|
|
self.picture = picture.map(Into::into);
|
|
self
|
|
}
|
|
}
|
|
|
|
impl Sizable for Avatar {
|
|
fn apply_size(&mut self, size: Size) {
|
|
self.size = size;
|
|
}
|
|
}
|
|
|
|
impl gpui::Styled for Avatar {
|
|
fn style(&mut self) -> &mut gpui::StyleRefinement {
|
|
&mut self.style
|
|
}
|
|
}
|
|
|
|
impl RenderOnce for Avatar {
|
|
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
|
let theme = cx.theme();
|
|
let radius = theme.radius.control;
|
|
let side = theme.sizes.resolve(self.size).avatar_size;
|
|
|
|
let base = h_flex()
|
|
.id(ElementId::from(self.id.clone()))
|
|
.flex_none()
|
|
.justify_center()
|
|
.overflow_hidden()
|
|
.size(side)
|
|
.text_size(side * 0.44)
|
|
.bg(theme.colors.secondary)
|
|
.rounded(radius)
|
|
.when_some(
|
|
self.name.clone().or(Some(self.id.clone())),
|
|
|avatar, label| avatar.role(Role::Image).aria_label(label),
|
|
)
|
|
.refine_style(&self.style);
|
|
|
|
match self.picture {
|
|
Some(url) => base.child(
|
|
gpui::img(gpui::SharedUri::from(url))
|
|
.size_full()
|
|
.rounded(radius),
|
|
),
|
|
None => base.child(PixelAvatar {
|
|
seed: fnv1a(self.id.as_bytes()),
|
|
side,
|
|
style: self.style,
|
|
}),
|
|
}
|
|
.into_any_element()
|
|
}
|
|
}
|
|
|
|
/// A deterministic, offline pixel-art avatar.
|
|
#[derive(IntoElement)]
|
|
struct PixelAvatar {
|
|
seed: u64,
|
|
side: gpui::Pixels,
|
|
style: StyleRefinement,
|
|
}
|
|
|
|
impl gpui::Styled for PixelAvatar {
|
|
fn style(&mut self) -> &mut StyleRefinement {
|
|
&mut self.style
|
|
}
|
|
}
|
|
|
|
impl RenderOnce for PixelAvatar {
|
|
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
|
let theme = cx.theme();
|
|
let radius = theme.radius.control;
|
|
let pattern = pattern(self.seed);
|
|
let mut cells = Vec::new();
|
|
|
|
let hue = (self.seed as f32 / u64::MAX as f32) * 360.0;
|
|
let accent = Oklch::from_hsla(theme.colors.accent);
|
|
let main = Oklch { h: hue, ..accent }.to_hsla();
|
|
|
|
let shade = if theme.variant == ThemeVariant::Dark {
|
|
Oklch {
|
|
l: (accent.l * 1.6).min(0.95),
|
|
..Oklch::from_hsla(main)
|
|
}
|
|
.to_hsla()
|
|
} else {
|
|
Oklch {
|
|
l: (accent.l * 0.45).max(0.18),
|
|
..Oklch::from_hsla(main)
|
|
}
|
|
.to_hsla()
|
|
};
|
|
|
|
for row in 0..GRID_SIZE {
|
|
for col in 0..GRID_SIZE {
|
|
let value = pattern[row * GRID_SIZE + col];
|
|
if value != 0 {
|
|
let color = if value == 2 { shade } else { main };
|
|
cells.push(
|
|
div()
|
|
.row_start(row as i16 + 1)
|
|
.row_end(row as i16 + 2)
|
|
.col_start(col as i16 + 1)
|
|
.col_end(col as i16 + 2)
|
|
.bg(color),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
div()
|
|
.grid()
|
|
.grid_cols(GRID_SIZE as u16)
|
|
.grid_rows(GRID_SIZE as u16)
|
|
.size(self.side)
|
|
.flex_none()
|
|
.rounded(radius)
|
|
.overflow_hidden()
|
|
.bg(gpui::Hsla { a: 0.16, ..main })
|
|
.children(cells)
|
|
.refine_style(&self.style)
|
|
}
|
|
}
|
|
|
|
fn pattern(seed: u64) -> [u8; GRID_SIZE * GRID_SIZE] {
|
|
let mut rng = PixelRng::new(seed);
|
|
let mut pattern = [0u8; GRID_SIZE * GRID_SIZE];
|
|
let mut filled = 0usize;
|
|
|
|
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 shade = rng.chance(SHADE_PROBABILITY);
|
|
set_cell(&mut pattern, row, col, if shade { 2 } else { 1 });
|
|
filled += 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
if filled < MIN_FILLED {
|
|
let total = art_rows * art_columns;
|
|
let start = (rng.next() % total as u64) as usize;
|
|
|
|
for offset in 0..total {
|
|
if filled >= MIN_FILLED {
|
|
break;
|
|
}
|
|
|
|
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);
|
|
filled += 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
pattern
|
|
}
|
|
|
|
/// Fill `cell (row, col)` and its horizontal mirror.
|
|
fn set_cell(pattern: &mut [u8; GRID_SIZE * GRID_SIZE], row: usize, col: usize, value: u8) {
|
|
pattern[row * GRID_SIZE + col] = value;
|
|
pattern[row * GRID_SIZE + (GRID_SIZE - 1 - col)] = value;
|
|
}
|
|
|
|
/// FNV-1a 64-bit hash, stable across platforms and runs.
|
|
fn fnv1a(bytes: &[u8]) -> u64 {
|
|
let mut hash = 0xcbf2_9ce4_8422_2325u64;
|
|
for &byte in bytes {
|
|
hash ^= byte as u64;
|
|
hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
|
|
}
|
|
hash
|
|
}
|
|
|
|
/// Tiny xorshift64* PRNG for deriving the pattern from the seed.
|
|
struct PixelRng(u64);
|
|
|
|
impl PixelRng {
|
|
fn new(seed: u64) -> Self {
|
|
Self(seed.max(1))
|
|
}
|
|
|
|
fn next(&mut self) -> u64 {
|
|
let mut x = self.0;
|
|
x ^= x >> 12;
|
|
x ^= x << 25;
|
|
x ^= x >> 27;
|
|
self.0 = x;
|
|
x.wrapping_mul(0x2545_f491_4f6c_dd1d)
|
|
}
|
|
|
|
fn chance(&mut self, probability: f32) -> bool {
|
|
self.next() as f32 / (u64::MAX as f32) < probability
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn the_pattern_is_mirror_symmetric() {
|
|
for seed in [0u64, 1, 42, u64::MAX] {
|
|
let pattern = pattern(seed);
|
|
for row in 0..GRID_SIZE {
|
|
for col in 0..GRID_SIZE / 2 {
|
|
assert_eq!(
|
|
pattern[row * GRID_SIZE + col],
|
|
pattern[row * GRID_SIZE + (GRID_SIZE - 1 - col)],
|
|
"seed {seed} row {row} col {col}"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn the_pattern_keeps_the_outer_ring_empty() {
|
|
for seed in [0u64, 1, 42, u64::MAX] {
|
|
let pattern = pattern(seed);
|
|
for col in 0..GRID_SIZE {
|
|
assert_eq!(pattern[col], 0, "top row, seed {seed}");
|
|
assert_eq!(pattern[(GRID_SIZE - 1) * GRID_SIZE + col], 0, "bottom row");
|
|
}
|
|
for row in 0..GRID_SIZE {
|
|
assert_eq!(pattern[row * GRID_SIZE], 0, "left column, seed {seed}");
|
|
assert_eq!(pattern[row * GRID_SIZE + GRID_SIZE - 1], 0, "right column");
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn the_pattern_is_deterministic_and_fills_the_minimum() {
|
|
for seed in [0u64, 7, 42, u64::MAX] {
|
|
let first = pattern(seed);
|
|
assert_eq!(first, pattern(seed), "seed {seed} must be stable");
|
|
|
|
// Mirrored cells count twice, so both halves carry the minimum.
|
|
let filled = first.iter().filter(|cell| **cell != 0).count();
|
|
assert!(filled >= MIN_FILLED * 2, "seed {seed} filled {filled}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn fnv1a_is_stable_across_calls() {
|
|
assert_eq!(fnv1a(b"reya"), fnv1a(b"reya"));
|
|
assert_ne!(fnv1a(b"reya"), fnv1a(b"signed"));
|
|
}
|
|
}
|