chore: refactor avatar component
This commit is contained in:
@@ -26,6 +26,7 @@
|
|||||||
- Restructure the core crate into focused modules for announcements, pull requests, git events, inbox, and upstream state
|
- Restructure the core crate into focused modules for announcements, pull requests, git events, inbox, and upstream state
|
||||||
- Publish nostr events directly through the client with relay acceptance required, removing the grasp push helper and its per-server retry loop so failures surface immediately
|
- Publish nostr events directly through the client with relay acceptance required, removing the grasp push helper and its per-server retry loop so failures surface immediately
|
||||||
- Poll the repository sync drift check every 180 seconds instead of 60
|
- Poll the repository sync drift check every 180 seconds instead of 60
|
||||||
|
- Merge the `PixelAvatar` into the `Avatar` component. Seed user pixel avatars from the public key instead of the display name, so avatars stay stable when a user renames
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
|||||||
+206
-25
@@ -1,37 +1,62 @@
|
|||||||
|
use std::fmt::Display;
|
||||||
|
|
||||||
use gpui::prelude::*;
|
use gpui::prelude::*;
|
||||||
use gpui::{App, SharedString, StyleRefinement, Window};
|
use gpui::{App, Pixels, SharedString, StyleRefinement, Window, div, px};
|
||||||
use gpui_base::{Avatar as BaseAvatar, AvatarFallback, AvatarImage};
|
use gpui_base::{AvatarImage, StyledExt};
|
||||||
use gpui_component::{ActiveTheme, Sizable, Size, StyledExt};
|
use gpui_component::{ActiveTheme, Colorize, Sizable, Size};
|
||||||
|
|
||||||
use crate::pixel_avatar::{PixelAvatar, side_length};
|
/// 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 accent shade instead of the main color.
|
||||||
|
const ACCENT_PROBABILITY: f32 = 0.25;
|
||||||
|
/// Minimum number of filled left-half cells.
|
||||||
|
const MIN_FILLED: usize = 5;
|
||||||
|
|
||||||
/// A user avatar built on the unstyled [`gpui_base::Avatar`].
|
/// An avatar for a user or other entity.
|
||||||
///
|
|
||||||
/// 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)]
|
#[derive(IntoElement)]
|
||||||
pub struct Avatar {
|
pub struct Avatar {
|
||||||
name: SharedString,
|
id: SharedString,
|
||||||
|
name: Option<SharedString>,
|
||||||
picture: Option<SharedString>,
|
picture: Option<SharedString>,
|
||||||
size: Size,
|
size: Size,
|
||||||
style: StyleRefinement,
|
style: StyleRefinement,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Avatar {
|
impl Avatar {
|
||||||
/// Create an avatar for `name`.
|
/// Create an avatar for `id`, a public key or any other stable string.
|
||||||
///
|
///
|
||||||
/// The name seeds the pixel fallback shown when no picture is set.
|
/// The id seeds the pixel fallback shown when no picture is set.
|
||||||
pub fn new(name: impl Into<SharedString>) -> Self {
|
pub fn new<T>(id: T) -> Self
|
||||||
|
where
|
||||||
|
T: Display,
|
||||||
|
{
|
||||||
Self {
|
Self {
|
||||||
name: name.into(),
|
id: id.to_string().into(),
|
||||||
|
name: None,
|
||||||
picture: None,
|
picture: None,
|
||||||
size: Size::Small,
|
size: Size::Small,
|
||||||
style: StyleRefinement::default(),
|
style: StyleRefinement::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The display name of the avatar's subject, if known.
|
||||||
|
pub fn name<T>(mut self, name: T) -> Self
|
||||||
|
where
|
||||||
|
T: Into<Option<SharedString>>,
|
||||||
|
{
|
||||||
|
self.name = name.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// The user's picture URL, if known.
|
/// The user's picture URL, if known.
|
||||||
pub fn picture(mut self, picture: Option<impl Into<SharedString>>) -> Self {
|
pub fn picture<T>(mut self, picture: Option<T>) -> Self
|
||||||
|
where
|
||||||
|
T: Into<SharedString>,
|
||||||
|
{
|
||||||
self.picture = picture.map(Into::into);
|
self.picture = picture.map(Into::into);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
@@ -52,20 +77,176 @@ impl Styled for Avatar {
|
|||||||
|
|
||||||
impl RenderOnce for Avatar {
|
impl RenderOnce for Avatar {
|
||||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||||
let fallback = AvatarFallback::new()
|
let radius = cx.theme().radius;
|
||||||
.size_full()
|
|
||||||
.child(PixelAvatar::new(self.name.clone()).size_full());
|
|
||||||
|
|
||||||
BaseAvatar::new()
|
match self.picture {
|
||||||
.size(side_length(self.size))
|
Some(url) => div()
|
||||||
|
.size(side_length(self.size))
|
||||||
|
.flex_shrink_0()
|
||||||
|
.rounded(radius)
|
||||||
|
.overflow_hidden()
|
||||||
|
.bg(cx.theme().secondary)
|
||||||
|
.child(AvatarImage::new(url).size_full().rounded(radius))
|
||||||
|
.refine_style(&self.style)
|
||||||
|
.into_any_element(),
|
||||||
|
None => PixelAvatar {
|
||||||
|
seed: fnv1a(self.id.as_bytes()),
|
||||||
|
side: side_length(self.size),
|
||||||
|
style: self.style,
|
||||||
|
}
|
||||||
|
.into_any_element(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A deterministic, offline pixel-art avatar.
|
||||||
|
#[derive(IntoElement)]
|
||||||
|
struct PixelAvatar {
|
||||||
|
seed: u64,
|
||||||
|
side: Pixels,
|
||||||
|
style: StyleRefinement,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl 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 pattern = pattern(self.seed);
|
||||||
|
let mut cells = Vec::new();
|
||||||
|
|
||||||
|
let hue = self.seed as f32 / u64::MAX as f32;
|
||||||
|
let main = theme.blue.hue(hue);
|
||||||
|
|
||||||
|
let shade = if theme.is_dark() {
|
||||||
|
main.lightness((main.l * 1.6).min(0.95))
|
||||||
|
} else {
|
||||||
|
main.lightness((main.l * 0.45).max(0.18))
|
||||||
|
};
|
||||||
|
|
||||||
|
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_shrink_0()
|
.flex_shrink_0()
|
||||||
.rounded(cx.theme().radius)
|
.rounded(theme.radius)
|
||||||
.overflow_hidden()
|
.overflow_hidden()
|
||||||
.bg(cx.theme().secondary)
|
.bg(main.opacity(0.16))
|
||||||
.when_some(self.picture, |this, url| {
|
.children(cells)
|
||||||
this.image(AvatarImage::new(url).size_full().rounded(cx.theme().radius))
|
|
||||||
})
|
|
||||||
.fallback(fallback)
|
|
||||||
.refine_style(&self.style)
|
.refine_style(&self.style)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The rendered side length of an avatar at `size`.
|
||||||
|
fn side_length(size: Size) -> Pixels {
|
||||||
|
match size {
|
||||||
|
Size::XSmall => px(16.),
|
||||||
|
Size::Small => px(24.),
|
||||||
|
Size::Medium => px(48.),
|
||||||
|
Size::Large => px(80.),
|
||||||
|
Size::Size(size) => size,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
// 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 });
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
mod avatar;
|
mod avatar;
|
||||||
mod dropdown_button;
|
mod dropdown_button;
|
||||||
mod nav_item;
|
mod nav_item;
|
||||||
mod pixel_avatar;
|
|
||||||
mod placeholder;
|
mod placeholder;
|
||||||
mod ref_selector;
|
mod ref_selector;
|
||||||
mod segment_button;
|
mod segment_button;
|
||||||
@@ -16,7 +15,6 @@ pub use avatar::Avatar;
|
|||||||
pub use copy_row::{copy_row, menu_copy_row};
|
pub use copy_row::{copy_row, menu_copy_row};
|
||||||
pub use dropdown_button::DropdownButton;
|
pub use dropdown_button::DropdownButton;
|
||||||
pub use nav_item::NavItem;
|
pub use nav_item::NavItem;
|
||||||
pub use pixel_avatar::PixelAvatar;
|
|
||||||
pub use placeholder::placeholder;
|
pub use placeholder::placeholder;
|
||||||
pub use ref_selector::ref_selector_trigger;
|
pub use ref_selector::ref_selector_trigger;
|
||||||
pub use segment_button::{CountBadge, SegmentButton};
|
pub use segment_button::{CountBadge, SegmentButton};
|
||||||
|
|||||||
@@ -1,219 +0,0 @@
|
|||||||
use gpui::prelude::*;
|
|
||||||
use gpui::{App, Pixels, StyleRefinement, Window, div, px};
|
|
||||||
use gpui_base::StyledExt;
|
|
||||||
use gpui_component::{ActiveTheme, Colorize, Sizable, Size};
|
|
||||||
|
|
||||||
/// Number of rows and columns in the pixel grid.
|
|
||||||
const GRID_SIZE: usize = 8;
|
|
||||||
/// 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;
|
|
||||||
/// Minimum number of filled left-half cells.
|
|
||||||
const MIN_FILLED: usize = 5;
|
|
||||||
|
|
||||||
/// A deterministic, offline pixel-art avatar.
|
|
||||||
#[derive(IntoElement)]
|
|
||||||
pub struct PixelAvatar {
|
|
||||||
seed: u64,
|
|
||||||
size: Size,
|
|
||||||
style: StyleRefinement,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PixelAvatar {
|
|
||||||
/// Create an avatar seeded from `seed`.
|
|
||||||
pub fn new(seed: impl AsRef<str>) -> Self {
|
|
||||||
Self {
|
|
||||||
seed: fnv1a(seed.as_ref().as_bytes()),
|
|
||||||
size: Size::Small,
|
|
||||||
style: StyleRefinement::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Sizable for PixelAvatar {
|
|
||||||
fn with_size(mut self, size: impl Into<Size>) -> Self {
|
|
||||||
self.size = size.into();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl 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 pattern = pattern(self.seed);
|
|
||||||
let mut cells = Vec::new();
|
|
||||||
|
|
||||||
let hue = self.seed as f32 / u64::MAX as f32;
|
|
||||||
let main = theme.blue.hue(hue);
|
|
||||||
|
|
||||||
let shade = if theme.is_dark() {
|
|
||||||
main.lightness((main.l * 1.6).min(0.95))
|
|
||||||
} else {
|
|
||||||
main.lightness((main.l * 0.45).max(0.18))
|
|
||||||
};
|
|
||||||
|
|
||||||
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(side_length(self.size))
|
|
||||||
.flex_shrink_0()
|
|
||||||
.rounded(theme.radius)
|
|
||||||
.overflow_hidden()
|
|
||||||
.bg(main.opacity(0.16))
|
|
||||||
.children(cells)
|
|
||||||
.refine_style(&self.style)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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.),
|
|
||||||
Size::Medium => px(48.),
|
|
||||||
Size::Large => px(80.),
|
|
||||||
Size::Size(size) => size,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
// 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 });
|
|
||||||
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::*;
|
|
||||||
|
|
||||||
fn count_filled(pattern: &[u8; GRID_SIZE * GRID_SIZE]) -> usize {
|
|
||||||
pattern.iter().filter(|&&cell| cell != 0).count()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn pattern_properties() {
|
|
||||||
for seed in 0..50 {
|
|
||||||
let pattern = pattern(seed);
|
|
||||||
assert!(
|
|
||||||
count_filled(&pattern) >= MIN_FILLED * 2,
|
|
||||||
"pattern too sparse for seed {seed}"
|
|
||||||
);
|
|
||||||
for row in 0..GRID_SIZE {
|
|
||||||
for col in 0..GRID_SIZE {
|
|
||||||
assert_eq!(
|
|
||||||
pattern[row * GRID_SIZE + col],
|
|
||||||
pattern[row * GRID_SIZE + (GRID_SIZE - 1 - col)],
|
|
||||||
"asymmetric pattern for seed {seed} at ({row}, {col})"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for seed in [0, 1, 42, u64::MAX] {
|
|
||||||
assert_eq!(pattern(seed), pattern(seed));
|
|
||||||
}
|
|
||||||
assert_ne!(pattern(42), pattern(43));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -71,7 +71,7 @@ pub(crate) fn sidebar_section(
|
|||||||
h_flex()
|
h_flex()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.items_center()
|
.items_center()
|
||||||
.child(Avatar::new(name.clone()).picture(picture))
|
.child(Avatar::new(*pubkey).name(name.clone()).picture(picture))
|
||||||
.child(div().text_sm().truncate().text_ellipsis().child(name))
|
.child(div().text_sm().truncate().text_ellipsis().child(name))
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
})),
|
})),
|
||||||
@@ -137,7 +137,11 @@ pub(crate) fn comments_section(store: &Entity<RepoStore>, root: EventId, cx: &Ap
|
|||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.child(Avatar::new(author.clone()).picture(picture))
|
.child(
|
||||||
|
Avatar::new(comment.pubkey)
|
||||||
|
.name(author.clone())
|
||||||
|
.picture(picture),
|
||||||
|
)
|
||||||
.child(author),
|
.child(author),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
|
|||||||
@@ -544,7 +544,8 @@ fn sub_activity(event: &Event, me: Option<PublicKey>, cx: &App) -> AnyElement {
|
|||||||
.items_center()
|
.items_center()
|
||||||
.text_xs()
|
.text_xs()
|
||||||
.child(
|
.child(
|
||||||
Avatar::new(name.clone())
|
Avatar::new(event.pubkey)
|
||||||
|
.name(name.clone())
|
||||||
.picture(profile.picture())
|
.picture(profile.picture())
|
||||||
.xsmall(),
|
.xsmall(),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ impl Render for IssueDetailView {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
let (title, author, picture, status, age, issue_id, content) = {
|
let (title, author, picture, status, age, issue_id, content, pubkey) = {
|
||||||
let profile_store = ProfileStore::global(cx);
|
let profile_store = ProfileStore::global(cx);
|
||||||
let profile = profile_store.read(cx).get(&issue.pubkey);
|
let profile = profile_store.read(cx).get(&issue.pubkey);
|
||||||
let content = if issue.content.is_empty() {
|
let content = if issue.content.is_empty() {
|
||||||
@@ -110,6 +110,7 @@ impl Render for IssueDetailView {
|
|||||||
relative_time(issue.created_at),
|
relative_time(issue.created_at),
|
||||||
issue.id,
|
issue.id,
|
||||||
content,
|
content,
|
||||||
|
issue.pubkey,
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -153,7 +154,7 @@ impl Render for IssueDetailView {
|
|||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.child(Avatar::new(&author).picture(picture))
|
.child(Avatar::new(pubkey).name(author.clone()).picture(picture))
|
||||||
.child(author),
|
.child(author),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
|
|||||||
@@ -189,7 +189,11 @@ impl IssuesView {
|
|||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.child(Avatar::new(author.clone()).picture(picture))
|
.child(
|
||||||
|
Avatar::new(issue.pubkey)
|
||||||
|
.name(author.clone())
|
||||||
|
.picture(picture),
|
||||||
|
)
|
||||||
.child(div().child(author)),
|
.child(div().child(author)),
|
||||||
)
|
)
|
||||||
.child(SharedString::from("opened"))
|
.child(SharedString::from("opened"))
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ pub(crate) mod sidebar;
|
|||||||
mod status_list;
|
mod status_list;
|
||||||
pub(crate) mod tree;
|
pub(crate) mod tree;
|
||||||
|
|
||||||
|
use std::fmt::Display;
|
||||||
|
|
||||||
use gpui::prelude::*;
|
use gpui::prelude::*;
|
||||||
use gpui::{AnyElement, App, div};
|
use gpui::{AnyElement, App, div};
|
||||||
use gpui_component::{Sizable as _, h_flex};
|
use gpui_component::{Sizable as _, h_flex};
|
||||||
@@ -22,7 +24,7 @@ pub(crate) use repo::{RepoItem, open_repo_item, open_repo_panel};
|
|||||||
pub use repo_list::RepoListView;
|
pub use repo_list::RepoListView;
|
||||||
pub use sidebar::SidebarPanel;
|
pub use sidebar::SidebarPanel;
|
||||||
use signed_state::{ProfileStore, RepoStore};
|
use signed_state::{ProfileStore, RepoStore};
|
||||||
use signed_ui::{Avatar, PixelAvatar};
|
use signed_ui::Avatar;
|
||||||
|
|
||||||
pub(crate) fn tab_title(avatar: AnyElement, label: impl IntoElement) -> impl IntoElement {
|
pub(crate) fn tab_title(avatar: AnyElement, label: impl IntoElement) -> impl IntoElement {
|
||||||
h_flex()
|
h_flex()
|
||||||
@@ -32,8 +34,8 @@ pub(crate) fn tab_title(avatar: AnyElement, label: impl IntoElement) -> impl Int
|
|||||||
.child(label)
|
.child(label)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn panel_avatar(seed: impl AsRef<str>) -> AnyElement {
|
pub(crate) fn panel_avatar(seed: impl Display) -> AnyElement {
|
||||||
PixelAvatar::new(seed).xsmall().into_any_element()
|
Avatar::new(seed).xsmall().into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn repo_tab_avatar(store: &RepoStore, cx: &App) -> AnyElement {
|
pub(crate) fn repo_tab_avatar(store: &RepoStore, cx: &App) -> AnyElement {
|
||||||
@@ -55,11 +57,13 @@ pub(crate) fn repo_tab_avatar(store: &RepoStore, cx: &App) -> AnyElement {
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|announcement| ProfileStore::global(cx).read(cx).get(&announcement.owner));
|
.map(|announcement| ProfileStore::global(cx).read(cx).get(&announcement.owner));
|
||||||
|
|
||||||
match owner.and_then(|profile| profile.picture().map(|picture| (profile.name(), picture))) {
|
let (name, picture) = owner
|
||||||
Some((name, picture)) => Avatar::new(name)
|
.map(|profile| (Some(profile.name()), profile.picture()))
|
||||||
.picture(Some(picture))
|
.unwrap_or((None, None));
|
||||||
.xsmall()
|
|
||||||
.into_any_element(),
|
Avatar::new(seed)
|
||||||
None => PixelAvatar::new(seed).xsmall().into_any_element(),
|
.name(name)
|
||||||
}
|
.picture(picture)
|
||||||
|
.xsmall()
|
||||||
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -407,7 +407,7 @@ impl PullRequestDetailView {
|
|||||||
return placeholder(&error, cx);
|
return placeholder(&error, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
let (author, picture, age, root_id) = {
|
let (author, picture, age, root_id, pubkey) = {
|
||||||
let store = self.store.read(cx);
|
let store = self.store.read(cx);
|
||||||
let Some(root) = store
|
let Some(root) = store
|
||||||
.pull_requests
|
.pull_requests
|
||||||
@@ -422,6 +422,7 @@ impl PullRequestDetailView {
|
|||||||
profile.picture(),
|
profile.picture(),
|
||||||
relative_time(root.created_at),
|
relative_time(root.created_at),
|
||||||
root.id,
|
root.id,
|
||||||
|
root.pubkey,
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -451,7 +452,8 @@ impl PullRequestDetailView {
|
|||||||
h_flex()
|
h_flex()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.child(
|
.child(
|
||||||
Avatar::new(author.clone())
|
Avatar::new(pubkey)
|
||||||
|
.name(author.clone())
|
||||||
.picture(picture),
|
.picture(picture),
|
||||||
)
|
)
|
||||||
.child(author),
|
.child(author),
|
||||||
|
|||||||
@@ -203,7 +203,11 @@ impl PullRequestsView {
|
|||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.child(Avatar::new(author.clone()).picture(picture))
|
.child(
|
||||||
|
Avatar::new(pr.pubkey)
|
||||||
|
.name(author.clone())
|
||||||
|
.picture(picture),
|
||||||
|
)
|
||||||
.child(div().child(author)),
|
.child(div().child(author)),
|
||||||
)
|
)
|
||||||
.child(SharedString::from("opened"))
|
.child(SharedString::from("opened"))
|
||||||
|
|||||||
@@ -174,7 +174,7 @@ fn maintainers(maintainers: &[PublicKey], cx: &App) -> AnyElement {
|
|||||||
.gap_2()
|
.gap_2()
|
||||||
.items_center()
|
.items_center()
|
||||||
.min_w_0()
|
.min_w_0()
|
||||||
.child(Avatar::new(name.clone()).picture(picture))
|
.child(Avatar::new(*pubkey).name(name.clone()).picture(picture))
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.flex_1()
|
.flex_1()
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ use signed_state::{
|
|||||||
SyncStatusStore,
|
SyncStatusStore,
|
||||||
};
|
};
|
||||||
use signed_ui::{
|
use signed_ui::{
|
||||||
Avatar, CountBadge, DropdownButton, PixelAvatar, copy_row, menu_copy_row, ref_selector_trigger,
|
Avatar, CountBadge, DropdownButton, copy_row, menu_copy_row, ref_selector_trigger,
|
||||||
};
|
};
|
||||||
use utils::middle_truncate;
|
use utils::middle_truncate;
|
||||||
|
|
||||||
@@ -896,7 +896,7 @@ impl RepoDetailView {
|
|||||||
|
|
||||||
let name = repo_display_name(self.store.read(cx));
|
let name = repo_display_name(self.store.read(cx));
|
||||||
let description = announcement.description();
|
let description = announcement.description();
|
||||||
let avatar = PixelAvatar::new(format!("{}:{}", announcement.owner, announcement.id));
|
let avatar = Avatar::new(format!("{}:{}", announcement.owner, announcement.id));
|
||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
.on_action(
|
.on_action(
|
||||||
@@ -1253,7 +1253,7 @@ impl RepoDetailView {
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|path| path.display().to_string())
|
.map(|path| path.display().to_string())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
let avatar = PixelAvatar::new(path.clone());
|
let avatar = Avatar::new(path.clone());
|
||||||
|
|
||||||
// A repository already bound to a coordinate is not offered for publishing again.
|
// A repository already bound to a coordinate is not offered for publishing again.
|
||||||
let bound = self.store.read(cx).nip34.clone();
|
let bound = self.store.read(cx).nip34.clone();
|
||||||
@@ -1481,7 +1481,9 @@ impl RepoDetailView {
|
|||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.child(Avatar::new(owner_name.clone()).picture(owner_picture))
|
.child(Avatar::new(announcement.owner)
|
||||||
|
.name(owner_name.clone())
|
||||||
|
.picture(owner_picture))
|
||||||
.child(div().text_xs().whitespace_nowrap().child(owner_name)),
|
.child(div().text_xs().whitespace_nowrap().child(owner_name)),
|
||||||
)
|
)
|
||||||
.when(!rest.is_empty(), |this| {
|
.when(!rest.is_empty(), |this| {
|
||||||
@@ -2180,12 +2182,17 @@ fn bound_repo_label(binding: &Nip34Binding, cx: &App) -> AnyElement {
|
|||||||
.child("Initialized by")
|
.child("Initialized by")
|
||||||
.text_color(cx.theme().muted_foreground)
|
.text_color(cx.theme().muted_foreground)
|
||||||
.when_some(profile, |this, profile| {
|
.when_some(profile, |this, profile| {
|
||||||
|
let name = profile.name();
|
||||||
this.child(
|
this.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.text_color(cx.theme().foreground)
|
.text_color(cx.theme().foreground)
|
||||||
.child(Avatar::new(profile.name()).picture(profile.picture()))
|
.child(
|
||||||
.child(profile.name()),
|
Avatar::new(profile.public_key())
|
||||||
|
.name(name.clone())
|
||||||
|
.picture(profile.picture()),
|
||||||
|
)
|
||||||
|
.child(name),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.when_some(binding.identifier.as_deref(), |this, ident| {
|
.when_some(binding.identifier.as_deref(), |this, ident| {
|
||||||
|
|||||||
@@ -274,7 +274,11 @@ impl RepoListView {
|
|||||||
h_flex()
|
h_flex()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.items_center()
|
.items_center()
|
||||||
.child(Avatar::new(owner.name()).picture(owner.picture()))
|
.child(
|
||||||
|
Avatar::new(announcement.owner)
|
||||||
|
.name(owner.name())
|
||||||
|
.picture(owner.picture()),
|
||||||
|
)
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.text_xs()
|
.text_xs()
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ use signed_state::{
|
|||||||
Backend, BackendEvent, CheckoutsStore, LocalReposStore, Nip34Binding, Nip34Kind, Profile,
|
Backend, BackendEvent, CheckoutsStore, LocalReposStore, Nip34Binding, Nip34Kind, Profile,
|
||||||
ProfileStore, RepoListStore, ResolvedLocalRepo, SyncStatusStore,
|
ProfileStore, RepoListStore, ResolvedLocalRepo, SyncStatusStore,
|
||||||
};
|
};
|
||||||
use signed_ui::{Avatar, NavItem, PixelAvatar, title_bar_drag_handlers};
|
use signed_ui::{Avatar, NavItem, title_bar_drag_handlers};
|
||||||
|
|
||||||
use super::{InboxView, PlaceholderPanel, RepoDetailView, RepoListView, open_repo_panel};
|
use super::{InboxView, PlaceholderPanel, RepoDetailView, RepoListView, open_repo_panel};
|
||||||
|
|
||||||
@@ -510,8 +510,7 @@ impl SidebarPanel {
|
|||||||
) -> impl IntoElement {
|
) -> impl IntoElement {
|
||||||
let name = announcement.name().map(SharedString::from);
|
let name = announcement.name().map(SharedString::from);
|
||||||
// The sidebar rows are compact, so the avatar matches the 16px nav icons.
|
// The sidebar rows are compact, so the avatar matches the 16px nav icons.
|
||||||
let avatar =
|
let avatar = Avatar::new(format!("{}:{}", announcement.owner, announcement.id)).xsmall();
|
||||||
PixelAvatar::new(format!("{}:{}", announcement.owner, announcement.id)).xsmall();
|
|
||||||
let announcement = announcement.clone();
|
let announcement = announcement.clone();
|
||||||
|
|
||||||
let addr = announcement.addr();
|
let addr = announcement.addr();
|
||||||
@@ -636,7 +635,11 @@ impl SidebarPanel {
|
|||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.child(Avatar::new(name.clone()).picture(picture))
|
.child(
|
||||||
|
Avatar::new(profile.public_key())
|
||||||
|
.name(name.clone())
|
||||||
|
.picture(picture),
|
||||||
|
)
|
||||||
.child(div().text_xs().font_semibold().child(name)),
|
.child(div().text_xs().font_semibold().child(name)),
|
||||||
)
|
)
|
||||||
.dropdown_menu(move |menu, _, _| {
|
.dropdown_menu(move |menu, _, _| {
|
||||||
@@ -768,7 +771,7 @@ pub(super) fn server_host(relay: &RelayUrl) -> SharedString {
|
|||||||
/// The repository's pixel avatar, with the bound owner's avatar at its bottom right.
|
/// The repository's pixel avatar, with the bound owner's avatar at its bottom right.
|
||||||
fn local_avatar(entry: &ResolvedLocalRepo, cx: &App) -> AnyElement {
|
fn local_avatar(entry: &ResolvedLocalRepo, cx: &App) -> AnyElement {
|
||||||
// Matches the other sidebar rows, and the badge below sits at its corner.
|
// Matches the other sidebar rows, and the badge below sits at its corner.
|
||||||
let avatar = PixelAvatar::new(entry.path.to_string_lossy()).xsmall();
|
let avatar = Avatar::new(entry.path.to_string_lossy()).xsmall();
|
||||||
|
|
||||||
let Some(owner) = entry.nip34.as_ref().and_then(|binding| binding.owner) else {
|
let Some(owner) = entry.nip34.as_ref().and_then(|binding| binding.owner) else {
|
||||||
return avatar.into_any_element();
|
return avatar.into_any_element();
|
||||||
@@ -782,7 +785,8 @@ fn local_avatar(entry: &ResolvedLocalRepo, cx: &App) -> AnyElement {
|
|||||||
.child(avatar)
|
.child(avatar)
|
||||||
.child(
|
.child(
|
||||||
div().absolute().bottom_neg_0p5().right_neg_0p5().child(
|
div().absolute().bottom_neg_0p5().right_neg_0p5().child(
|
||||||
Avatar::new(profile.name())
|
Avatar::new(owner)
|
||||||
|
.name(profile.name())
|
||||||
.picture(profile.picture())
|
.picture(profile.picture())
|
||||||
.size(px(14.)),
|
.size(px(14.)),
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user