update
This commit is contained in:
@@ -4,8 +4,8 @@ use gpui_component::clipboard::Clipboard;
|
||||
use gpui_component::menu::PopupMenuItem;
|
||||
use gpui_component::{ActiveTheme, StyledExt, h_flex};
|
||||
|
||||
/// A muted command row with a copy button: the value in a mono-friendly,
|
||||
/// truncated line, with a [`Clipboard`] button copying the full value.
|
||||
/// A muted command row with a copy button.
|
||||
/// The value renders truncated and a [`Clipboard`] button copies the full value.
|
||||
pub fn copy_row<E>(copy_id: E, command: &SharedString, cx: &App) -> Div
|
||||
where
|
||||
E: Into<ElementId>,
|
||||
@@ -34,10 +34,11 @@ where
|
||||
)
|
||||
}
|
||||
|
||||
/// One row of a copy menu: a small title above the compact label, with
|
||||
/// a copy button that flips to a check while the value is on the clipboard.
|
||||
/// Clicking the row copies and dismisses the menu; the copy button stops
|
||||
/// propagation so the menu stays open. Both copy `copy`, never the label.
|
||||
/// One row of a copy menu, with a small title above the compact label.
|
||||
/// A copy button flips to a check while the value is on the clipboard.
|
||||
/// Clicking the row copies and dismisses the menu.
|
||||
/// The copy button stops propagation so the menu stays open.
|
||||
/// Both copy `copy`, never the label.
|
||||
pub fn menu_copy_row(
|
||||
id: &'static str,
|
||||
title: &'static str,
|
||||
|
||||
@@ -7,11 +7,10 @@ use gpui_base::{Button as BaseButton, Popover, Selectable, StyledExt};
|
||||
use gpui_component::menu::PopupMenu;
|
||||
use gpui_component::{ActiveTheme, Icon, IconName, Sizable, h_flex};
|
||||
|
||||
/// A split dropdown button built on `gpui_base::Popover`: an action element
|
||||
/// with a separate caret trigger that opens a [`PopupMenu`].
|
||||
///
|
||||
/// The action and the caret are caller-supplied elements, so the look stays
|
||||
/// in the application; this component only owns the popover wiring.
|
||||
/// A split dropdown button built on `gpui_base::Popover`.
|
||||
/// An action element with a separate caret trigger that opens a [`PopupMenu`].
|
||||
/// The action and the caret are caller-supplied elements, so the look stays in the app.
|
||||
/// This component only owns the popover wiring.
|
||||
#[derive(IntoElement)]
|
||||
pub struct DropdownButton {
|
||||
id: ElementId,
|
||||
@@ -38,15 +37,16 @@ impl DropdownButton {
|
||||
}
|
||||
}
|
||||
|
||||
/// The action half of the button. It keeps its own icon, label, tooltip
|
||||
/// and click handler.
|
||||
/// The action half of the button.
|
||||
/// It keeps its own icon, label, tooltip and click handler.
|
||||
pub fn action(mut self, action: impl IntoElement + 'static) -> Self {
|
||||
self.action = Some(action.into_any_element());
|
||||
self
|
||||
}
|
||||
|
||||
/// The menu built by `builder` — the same signature as gpui-component's
|
||||
/// `DropdownButton::dropdown_menu`, so existing menu code keeps working.
|
||||
/// The menu built by `builder`.
|
||||
/// Matches gpui-component's `DropdownButton::dropdown_menu` signature.
|
||||
/// Existing menu code keeps working.
|
||||
pub fn dropdown_menu(
|
||||
mut self,
|
||||
builder: impl Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu + 'static,
|
||||
@@ -55,10 +55,9 @@ impl DropdownButton {
|
||||
self
|
||||
}
|
||||
|
||||
/// Which corner of the caret the menu anchors to. Defaults to
|
||||
/// [`Anchor::TopRight`], so the menu's right edge lines up with the
|
||||
/// caret's.
|
||||
#[allow(dead_code)] // API knob; current call sites use the default anchor.
|
||||
/// Which corner of the caret the menu anchors to.
|
||||
/// Defaults to [`Anchor::TopRight`], lining the menu's right edge up with the caret's.
|
||||
#[allow(dead_code)] // API knob, current call sites use the default anchor.
|
||||
pub fn anchor(mut self, anchor: impl Into<Anchor>) -> Self {
|
||||
self.anchor = anchor.into();
|
||||
self
|
||||
@@ -71,8 +70,8 @@ impl Styled for DropdownButton {
|
||||
}
|
||||
}
|
||||
|
||||
/// Holds the [`PopupMenu`] entity of one popover between renders. Dismissal
|
||||
/// drops it, so the menu is rebuilt with fresh items on the next open.
|
||||
/// Holds the [`PopupMenu`] entity of one popover between renders.
|
||||
/// Dismissal drops it, so the menu is rebuilt with fresh items on the next open.
|
||||
#[derive(Default)]
|
||||
struct DropdownMenuState {
|
||||
menu: Option<Entity<PopupMenu>>,
|
||||
@@ -85,7 +84,8 @@ impl RenderOnce for DropdownButton {
|
||||
"a DropdownButton needs a `dropdown_menu`"
|
||||
);
|
||||
|
||||
// The popover needs its own id: both the container and the popover register keyed state on this window.
|
||||
// The popover needs its own id.
|
||||
// The container and the popover both register keyed state on this window.
|
||||
let popover_id = SharedString::from(format!("{}-popover", self.id));
|
||||
let anchor = self.anchor;
|
||||
let menu_state =
|
||||
@@ -109,8 +109,8 @@ impl RenderOnce for DropdownButton {
|
||||
this.child(
|
||||
Popover::new(popover_id)
|
||||
.anchor(anchor)
|
||||
// The menu dismisses itself on outside click or Escape;
|
||||
// the subscription below closes the popover along with it.
|
||||
// The menu dismisses itself on outside click or Escape.
|
||||
// The subscription below closes the popover along with it.
|
||||
.overlay_closable(false)
|
||||
.trigger_with(caret)
|
||||
.content(
|
||||
@@ -148,8 +148,8 @@ impl RenderOnce for DropdownButton {
|
||||
}
|
||||
}
|
||||
|
||||
/// The default caret: a chevron button the height of a medium button, tinted
|
||||
/// by the theme, with hover and menu-open states.
|
||||
/// The default caret, a chevron button the height of a medium button.
|
||||
/// It is tinted by the theme and styled for hover and menu-open states.
|
||||
fn default_caret(id: impl Into<ElementId>, cx: &App) -> BaseButton {
|
||||
BaseButton::new(id)
|
||||
.h(px(32.))
|
||||
|
||||
@@ -7,8 +7,8 @@ use gpui::{
|
||||
ImageCacheItem, ImageCacheProvider, ImageSource, Resource, hash,
|
||||
};
|
||||
|
||||
/// Default number of images each view's cache retains. Loading a new image
|
||||
/// evicts the least recently used entry once this is reached.
|
||||
/// Default number of images each view's cache retains.
|
||||
/// Loading a new image evicts the least recently used entry once this is reached.
|
||||
pub const MAX_IMAGES: usize = 128;
|
||||
|
||||
pub fn image_cache(id: impl Into<ElementId>, max_items: usize) -> AppImageCacheProvider {
|
||||
|
||||
@@ -1,33 +1,3 @@
|
||||
//! Reusable UI components and elements for Signed.
|
||||
//!
|
||||
//! Everything here is presentation-only: the components read theme tokens
|
||||
//! through [`gpui_component::ActiveTheme`] and render with plain GPUI
|
||||
//! elements, so any view in the app can use them without depending on app
|
||||
//! state. They are built on the unstyled `gpui-base` primitives and the
|
||||
//! styled `gpui-component` library.
|
||||
//!
|
||||
//! The crate is organized by component:
|
||||
//!
|
||||
//! - [`PixelAvatar`] — deterministic, offline "pixel art" avatar
|
||||
//! - [`NavItem`] — sidebar navigation row (leading element + label + suffix)
|
||||
//! - [`DropdownButton`] — split button: an action plus a caret that opens a
|
||||
//! [`PopupMenu`], wired through `gpui_base::Popover`
|
||||
//! - [`SegmentButton`] / [`CountBadge`] — segmented filter/tab button with an
|
||||
//! optional count badge
|
||||
//! - [`UserAvatar`] — user picture avatar with a name-initials fallback
|
||||
//! - [`status_badge`] — NIP-34 issue/PR status badge
|
||||
//! - [`placeholder`] — centered muted placeholder message
|
||||
//! - [`copy_row`] / [`menu_copy_row`] — rows with a copy-to-clipboard button
|
||||
//! - [`tree_row`] — one row of a file tree
|
||||
//! - [`setting_row`] / [`setting_block`] — label + description rows for a
|
||||
//! settings dialog, with the control on the right (row) or below (block)
|
||||
//! - [`SelectOption`] — dropdown option with a display label and a stored
|
||||
//! value
|
||||
//! - [`title_bar_drag_handlers`] — make an element behave like a window
|
||||
//! title bar (drag moves the window, double-click zooms)
|
||||
//! - [`image_cache`] — per-view LRU image cache provider
|
||||
//! - [`middle_truncate`] — `[head]...[tail]` string truncation
|
||||
|
||||
mod dropdown_button;
|
||||
mod nav_item;
|
||||
mod pixel_avatar;
|
||||
|
||||
@@ -2,9 +2,10 @@ use gpui::prelude::*;
|
||||
use gpui::{App, ClickEvent, ElementId, SharedString, StyleRefinement, Window, div};
|
||||
use gpui_component::{ActiveTheme, StyledExt, h_flex};
|
||||
|
||||
/// A single navigation entry in a sidebar: an arbitrary leading element
|
||||
/// (an icon, avatar, ...) and a text label with a hover highlight,
|
||||
/// an optional trailing suffix (e.g. a status icon) and an optional click handler.
|
||||
/// A single navigation entry in a sidebar.
|
||||
/// It has an arbitrary leading element, such as an icon or avatar, and a text label.
|
||||
/// Hover highlights the row.
|
||||
/// It can carry a trailing suffix, such as a status icon, and an optional click handler.
|
||||
#[allow(clippy::type_complexity)]
|
||||
#[derive(IntoElement)]
|
||||
pub struct NavItem {
|
||||
@@ -12,7 +13,7 @@ pub struct NavItem {
|
||||
style: StyleRefinement,
|
||||
icon: gpui::AnyElement,
|
||||
label: SharedString,
|
||||
/// Trailing element rendered at the right edge of the row, after the (ellipsized) label.
|
||||
/// Trailing element at the right edge of the row, after the ellipsized label.
|
||||
suffix: Option<gpui::AnyElement>,
|
||||
on_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
|
||||
}
|
||||
|
||||
@@ -9,13 +9,15 @@ const GRID_SIZE: usize = 8;
|
||||
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, so a sparse roll still yields a
|
||||
/// recognizable shape (each left-half cell is mirrored to a right-half one).
|
||||
/// Minimum number of filled left-half cells.
|
||||
/// A sparse roll still yields a recognizable shape.
|
||||
/// Each left-half cell is mirrored to a right-half one.
|
||||
const MIN_FILLED: usize = 5;
|
||||
|
||||
/// A deterministic, offline "pixel art" avatar: an 8×8 grid with horizontal
|
||||
/// mirror symmetry, seeded from a stable string such as the repository id and
|
||||
/// owner public key. The same seed always renders the same avatar.
|
||||
/// A deterministic, offline pixel-art avatar.
|
||||
/// An 8×8 grid with horizontal mirror symmetry.
|
||||
/// Seeded from a stable string such as the repository id and owner public key.
|
||||
/// The same seed always renders the same avatar.
|
||||
#[derive(IntoElement)]
|
||||
pub struct PixelAvatar {
|
||||
seed: u64,
|
||||
@@ -24,8 +26,8 @@ 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.
|
||||
/// 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()),
|
||||
@@ -85,8 +87,9 @@ impl RenderOnce for PixelAvatar {
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate the 8×8 cell pattern for `seed`. Cells are `0` (empty), `1`
|
||||
/// (main color) or `2` (accent shade); the right half mirrors the left half.
|
||||
/// Generate the 8×8 cell pattern for `seed`.
|
||||
/// Cells are `0` for empty, `1` for main color and `2` for accent shade.
|
||||
/// The right half mirrors the left half.
|
||||
fn pattern(seed: u64) -> [u8; GRID_SIZE * GRID_SIZE] {
|
||||
let mut rng = PixelRng::new(seed);
|
||||
let mut pattern = [0u8; GRID_SIZE * GRID_SIZE];
|
||||
@@ -102,8 +105,8 @@ fn pattern(seed: u64) -> [u8; GRID_SIZE * GRID_SIZE] {
|
||||
}
|
||||
}
|
||||
|
||||
// Sparse rolls can come out nearly empty; top the pattern up to the
|
||||
// minimum fill, scanning from a seeded starting cell.
|
||||
// Sparse rolls can come out nearly empty.
|
||||
// Top the pattern up to the minimum fill, scanning from a seeded starting cell.
|
||||
if filled < MIN_FILLED {
|
||||
let half = GRID_SIZE * GRID_SIZE / 2;
|
||||
let start = (rng.next() % half as u64) as usize;
|
||||
@@ -130,7 +133,7 @@ fn set_cell(pattern: &mut [u8; GRID_SIZE * GRID_SIZE], row: usize, col: usize, v
|
||||
pattern[row * GRID_SIZE + (GRID_SIZE - 1 - col)] = value;
|
||||
}
|
||||
|
||||
/// FNV-1a 64-bit hash; stable across platforms and runs.
|
||||
/// 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 {
|
||||
|
||||
@@ -3,9 +3,9 @@ use gpui::{App, ClickEvent, ElementId, SharedString, StyleRefinement, Window, di
|
||||
use gpui_base::{Button as BaseButton, StyledExt};
|
||||
use gpui_component::ActiveTheme;
|
||||
|
||||
/// A small count badge shown after a label, e.g. on a segmented filter
|
||||
/// button ("All 12") or a tab. Rendered from theme tokens; sized for the
|
||||
/// compact header buttons it lives on.
|
||||
/// A small count badge shown after a label.
|
||||
/// Used on segmented filter buttons, like `All 12`, and on tabs.
|
||||
/// Rendered from theme tokens and sized for the compact header buttons it lives on.
|
||||
#[derive(IntoElement)]
|
||||
pub struct CountBadge {
|
||||
count: usize,
|
||||
@@ -46,12 +46,11 @@ impl RenderOnce for CountBadge {
|
||||
}
|
||||
}
|
||||
|
||||
/// A segmented filter/tab button: an icon, a label, an optional [`CountBadge`]
|
||||
/// and a selected (pressed) state, styled from the theme's button tokens.
|
||||
///
|
||||
/// Built on the unstyled `gpui_base::Button`, like the app's other custom
|
||||
/// controls; the `primary` variant uses the primary button tokens for
|
||||
/// call-to-action buttons ("New issue", "New PR").
|
||||
/// A segmented filter/tab button with an icon, a label and an optional [`CountBadge`].
|
||||
/// The selected state renders the button pressed, styled from theme button tokens.
|
||||
/// Built on the unstyled `gpui_base::Button`, like the app's other custom controls.
|
||||
/// The `primary` variant uses the primary button tokens.
|
||||
/// It suits call-to-action buttons such as `New issue` and `New PR`.
|
||||
#[allow(clippy::type_complexity)]
|
||||
#[derive(IntoElement)]
|
||||
pub struct SegmentButton {
|
||||
@@ -101,7 +100,7 @@ impl SegmentButton {
|
||||
self
|
||||
}
|
||||
|
||||
/// Use the primary button tokens (for call-to-action buttons).
|
||||
/// Use the primary button tokens, for call-to-action buttons.
|
||||
pub fn primary(mut self) -> Self {
|
||||
self.primary = true;
|
||||
self
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
//! Reusable settings UI: rows and blocks for building a settings dialog, plus
|
||||
//! a labeled dropdown option.
|
||||
|
||||
use gpui::prelude::*;
|
||||
use gpui::{App, SharedString, div};
|
||||
use gpui_component::searchable_list::SearchableListItem;
|
||||
use gpui_component::{ActiveTheme, StyledExt, h_flex, v_flex};
|
||||
|
||||
/// A dropdown option with a display label and a stored value.
|
||||
///
|
||||
/// Renders the `label` in the trigger and the menu, while `value` is what a
|
||||
/// [`gpui_component::select::SelectState`] reports as the selection.
|
||||
/// The trigger and menu render the `label`.
|
||||
/// The `value` is what [`gpui_component::select::SelectState`] reports as the selection.
|
||||
#[derive(Clone)]
|
||||
pub struct SelectOption {
|
||||
value: SharedString,
|
||||
@@ -48,7 +44,7 @@ impl SearchableListItem for SelectOption {
|
||||
}
|
||||
}
|
||||
|
||||
/// A settings row: label + description on the left, control on the right.
|
||||
/// A settings row with the label and description on the left and the control on the right.
|
||||
pub fn setting_row(
|
||||
cx: &App,
|
||||
title: impl Into<SharedString>,
|
||||
@@ -85,8 +81,8 @@ pub fn setting_row(
|
||||
)
|
||||
}
|
||||
|
||||
/// A full-width settings block: title + subtitle in one header, `gap_3`
|
||||
/// between the header and the control below.
|
||||
/// A full-width settings block with title and subtitle in one header.
|
||||
/// `gap_3` separates the header from the control below.
|
||||
pub fn setting_block(
|
||||
cx: &App,
|
||||
title: impl Into<SharedString>,
|
||||
|
||||
@@ -5,8 +5,8 @@ use gpui_component::tooltip::Tooltip;
|
||||
use gpui_component::{ActiveTheme, Icon, Sizable, v_flex};
|
||||
use signed_core::RepoStatus;
|
||||
|
||||
/// The status badge shown next to an issue or pull request: icon + colored square,
|
||||
/// with a tooltip describing the status.
|
||||
/// The status badge shown next to an issue or pull request.
|
||||
/// It has an icon and a colored square, with a tooltip describing the status.
|
||||
pub fn status_badge(status: RepoStatus, cx: &App) -> AnyElement {
|
||||
let (icon, label, tooltip, bg, fg) = match status {
|
||||
RepoStatus::Open => (
|
||||
|
||||
@@ -8,12 +8,12 @@ struct WindowDragState {
|
||||
should_move: bool,
|
||||
}
|
||||
|
||||
/// Make an element behave like a window title bar: dragging it moves the
|
||||
/// window, and double-clicking zooms the window (or performs the platform's
|
||||
/// default title-bar double-click action on macOS).
|
||||
///
|
||||
/// Only the bar's non-interactive areas should get this — tabs are draggable
|
||||
/// (to reorder panels) and must not move the window.
|
||||
/// Make an element behave like a window title bar.
|
||||
/// Dragging it moves the window.
|
||||
/// Double-clicking zooms the window.
|
||||
/// On macOS it runs the platform's default title-bar double-click action.
|
||||
/// Only the bar's non-interactive areas should get this.
|
||||
/// Tabs are draggable to reorder panels and must not move the window.
|
||||
pub fn title_bar_drag_handlers(
|
||||
this: Stateful<Div>,
|
||||
window: &mut Window,
|
||||
|
||||
@@ -4,8 +4,9 @@ use gpui_component::list::ListItem;
|
||||
use gpui_component::tree::TreeEntry;
|
||||
use gpui_component::{Icon, IconName, Sizable, h_flex};
|
||||
|
||||
/// One row of a file tree: icon + name, indented by depth.
|
||||
/// Clicking a file runs `on_click`; folders expand/collapse via the tree itself.
|
||||
/// One row of a file tree, an icon and a name indented by depth.
|
||||
/// Clicking a file runs `on_click`.
|
||||
/// Folders expand and collapse via the tree itself.
|
||||
pub fn tree_row<F>(ix: usize, entry: &TreeEntry, selected: bool, on_click: F) -> ListItem
|
||||
where
|
||||
F: Fn(&mut Window, &mut App) + 'static,
|
||||
|
||||
@@ -3,8 +3,8 @@ use gpui::{App, SharedString, StyleRefinement, Window};
|
||||
use gpui_component::avatar::Avatar;
|
||||
use gpui_component::{ActiveTheme, Sizable, StyledExt};
|
||||
|
||||
/// A user avatar: the gpui-component [`Avatar`] sized small and rounded with
|
||||
/// the theme radius, showing the user's picture or a name-initials fallback.
|
||||
/// 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.
|
||||
#[derive(IntoElement)]
|
||||
pub struct UserAvatar {
|
||||
name: SharedString,
|
||||
@@ -13,8 +13,8 @@ pub struct UserAvatar {
|
||||
}
|
||||
|
||||
impl UserAvatar {
|
||||
/// Create an avatar for `name`; the name seeds the initials fallback
|
||||
/// shown when no picture is set.
|
||||
/// Create an avatar for `name`.
|
||||
/// The name seeds the initials fallback shown when no picture is set.
|
||||
pub fn new(name: impl Into<SharedString>) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/// `[head chars]...[tail chars]` middle truncation; the value is left alone
|
||||
/// when it is too short for the ellipsis to save space.
|
||||
/// `[head chars]...[tail chars]` middle truncation.
|
||||
/// Values too short for the ellipsis to save space are left alone.
|
||||
pub fn middle_truncate(value: &str, head: usize, tail: usize) -> String {
|
||||
let len = value.chars().count();
|
||||
if len <= head + tail + 3 {
|
||||
@@ -32,7 +32,7 @@ mod tests {
|
||||
),
|
||||
"30617:a008...3564d:ngit"
|
||||
);
|
||||
// Too short to save space with the ellipsis: left alone.
|
||||
// Too short to save space with the ellipsis, left alone.
|
||||
assert_eq!(middle_truncate("short", 10, 10), "short");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user