1
0
forked from GRIN/grim

goblin: enable the activity-row identity badge (approved cue)

Turn the per-identity activity cue on (SHOW_ROW_IDENTITY_CUE = true) and render
it as the owner-approved corner badge. The row's main avatar stays the
COUNTERPARTY; overlaid on its bottom-right corner is a small ~14px badge filled
with the USER's OWN identity gradient for that transaction (from
ActivityItem.owner_pubkey via the exact identicon gradient math, falling back to
the primary identity for pre-feature rows), so a glance clusters which of your
identities each payment used. Shown only when the wallet holds more than one
identity, never on system (mining) rows.

The badge is a true smooth rotated linear gradient (an egui mesh reproducing the
avatar's SVG gradient, not a flat chip) at 0.9 fill opacity, with a 1px
theme-aware hairline ring (near-black on light, near-white on dark) so it stays
legible on both pure-white and pure-black and against the avatar it sits on.
identity_dot is now the single gradient renderer, shared by the row badge and
the transaction-detail legend.
This commit is contained in:
2ro
2026-07-05 16:46:59 -04:00
parent e56d9d05e2
commit 6d0dde523f
3 changed files with 73 additions and 47 deletions
+10
View File
@@ -96,6 +96,16 @@ pub fn gradient_rgb8(id: &str) -> ((u8, u8, u8), (u8, u8, u8)) {
(c1, c2)
}
/// The identity's gradient stops PLUS the rotation angle (degrees), so a small
/// egui-drawn badge can reproduce the same rotated linear gradient the SVG
/// avatar uses (`gradient_avatar_svg`: `x1=0 y1=0 x2=1 y2=1` then
/// `rotate(angle, 0.5, 0.5)`). `id` may be an npub or raw hex.
pub fn gradient_stops(id: &str) -> ((u8, u8, u8), (u8, u8, u8), f32) {
let hex = to_hex_seed(id);
let (c1, c2, angle) = gradient_rgb(&hex);
(c1, c2, angle as f32)
}
/// The gradient avatar as a standalone SVG document, seeded by `hex` (lowercase
/// hex pubkey). `id_suffix` makes the gradient element id unique when several
/// are inlined into ONE html document; for a standalone document (how egui
+15 -21
View File
@@ -130,15 +130,12 @@ pub struct GoblinWalletView {
wipe_confirm: bool,
}
/// Whether a per-identity cue is drawn on activity rows. OFF by design: on an
/// activity row the avatar is the COUNTERPARTY, so a dot in an identity gradient
/// there is both redundant with that avatar and does not convey which of the
/// USER's own identities was involved. A correct in-list cue would need a
/// distinct element encoding the user's own active identity, which is a separate
/// follow-up with its own design pass. The seam (this const + the plumbing below)
/// is kept so that cue can be added in one place; the detail-view identity
/// attribution is the shipped "which identity" answer. Leave false.
const SHOW_ROW_IDENTITY_CUE: bool = false;
/// Whether the per-identity cue is drawn on activity rows (owner-approved). The
/// row's main avatar stays the COUNTERPARTY; the cue is a SMALL corner badge on
/// that avatar, filled with the USER's OWN identity gradient for the tx (from
/// `ActivityItem.owner_pubkey`), so a glance clusters which of your identities
/// each payment used. Only shown when the wallet holds more than one identity.
const SHOW_ROW_IDENTITY_CUE: bool = true;
/// Per-frame identity context for the activity rows: whether the wallet holds
/// more than one identity (the cue only shows then) and the primary identity's
@@ -2132,22 +2129,19 @@ impl GoblinWalletView {
item.system,
tex.as_ref(),
);
// Provisional per-identity cue (owner reviews the look before it is final):
// only when the wallet holds MORE THAN ONE identity, and never on system
// (mining) rows. A single, quiet gradient dot at the row's top-left corner,
// seeded by the identity this tx used (its own gradient, matching that
// identity's avatar in the switcher). Behind SHOW_ROW_IDENTITY_CUE so it can
// be toned down or dropped in one place without touching the rest.
// Per-identity cue (owner-approved): only when the wallet holds MORE THAN
// ONE identity, and never on system (mining) rows. A small corner badge on
// the counterparty avatar, filled with the identity THIS tx used (its own
// gradient; falls back to the primary for pre-feature rows). The row avatar
// is 40px, flush to the row's left and vertically centred, so its
// bottom-right corner is at (left+40, mid+20); the badge overhangs that
// corner by ~4px (matching the mock's right:-4/bottom:-4, 14px badge).
if SHOW_ROW_IDENTITY_CUE && id_cue.multi && !item.system {
let seed = item.owner_pubkey.clone().or_else(|| id_cue.primary.clone());
if let Some(seed) = seed {
let r = resp.rect;
w::identity_dot(
ui.painter(),
egui::pos2(r.left() + 8.0, r.top() + 7.0),
4.0,
&seed,
);
let badge = egui::pos2(r.left() + 37.0, r.center().y + 17.0);
w::identity_dot(ui.painter(), badge, 6.0, &seed);
}
}
if resp.clicked() {
+48 -26
View File
@@ -1101,37 +1101,59 @@ pub fn info_row_dot(ui: &mut Ui, label: &str, value: &str, seed: &str) {
ui.add_space(8.0);
}
/// A deliberately QUIET per-identity cue: a small two-tone dot in an identity's
/// OWN gradient — the same pubkey-seeded math as its avatar (`identicon`), so the
/// dot reads as a legend for that identity and matches everywhere it appears
/// (activity row, switcher page, transaction detail). A faint, theme-aware ring
/// keeps a PALE gradient legible on a light background and a DARK one on a dark
/// background, without ever becoming a bright chip. `seed` is the identity's npub
/// or pubkey hex. This is the single shared renderer, so toning it down (or
/// removing the row cue) happens in exactly one place.
/// A per-identity cue: a small disc filled with an identity's OWN gradient — the
/// same pubkey-seeded, rotated two-stop linear gradient its avatar uses
/// (`identicon`), so the disc reads as a color legend for that identity and
/// matches its avatar everywhere it appears (activity-row corner badge, switcher,
/// transaction detail). The fill is a true smooth gradient (an egui mesh, not a
/// flat chip) at 0.9 opacity, with a 1px theme-aware hairline ring that keeps it
/// legible on both a pure-white and a pure-black background. `seed` is the
/// identity's npub or pubkey hex. Single shared renderer, so the look is tuned in
/// exactly one place. Matches the owner-approved cue mock precisely.
pub fn identity_dot(painter: &egui::Painter, center: egui::Pos2, radius: f32, seed: &str) {
let t = theme::tokens();
let ((r1, g1, b1), (r2, g2, b2)) = super::identicon::gradient_rgb8(seed);
// Faint fill in BOTH themes; the ring below carries legibility, not the fill.
let fill_a = if t.dark_base { 165 } else { 185 };
let c1 = Color32::from_rgba_unmultiplied(r1, g1, b1, fill_a);
let c2 = Color32::from_rgba_unmultiplied(r2, g2, b2, fill_a);
// Base disc in the first stop, then the second stop as a half-disc so the dot
// is two-tone like the gradient avatar rather than a flat chip.
painter.circle_filled(center, radius, c1);
let start = std::f32::consts::FRAC_PI_4; // 45 degrees, the gradient axis-ish
let mut pts = Vec::with_capacity(14);
for i in 0..=12 {
let a = start + std::f32::consts::PI * (i as f32 / 12.0);
pts.push(center + radius * egui::Vec2::angled(a));
let ((r1, g1, b1), (r2, g2, b2), angle) = super::identicon::gradient_stops(seed);
// 0.9 fill opacity per the mock.
const FILL_A: u8 = 230;
let lerp = |a: u8, b: u8, f: f32| (a as f32 + (b as f32 - a as f32) * f).round() as u8;
let col = |f: f32| {
let f = f.clamp(0.0, 1.0);
Color32::from_rgba_unmultiplied(lerp(r1, r2, f), lerp(g1, g2, f), lerp(b1, b2, f), FILL_A)
};
// Reproduce the SVG's rotated linear gradient (base axis 0,0 -> 1,1 in the
// unit bounding box, then rotate(angle) about the centre). For a rim point in
// direction (cv, sv) on the unit circle, its bounding-box offset from centre
// is (0.5·cv, 0.5·sv); inverse-rotating by the gradient angle and projecting
// onto the (1,1) axis gives the stop parameter t.
let (sin_a, cos_a) = angle.to_radians().sin_cos();
let t_at = |cv: f32, sv: f32| -> f32 {
let dx = 0.5 * cv;
let dy = 0.5 * sv;
let rx = cos_a * dx + sin_a * dy;
let ry = -sin_a * dx + cos_a * dy;
(rx + ry) * 0.5 + 0.5
};
// Gradient-filled disc as a triangle fan; egui interpolates the per-vertex
// colours, so the fill is a smooth gradient at this size.
let n = 28usize;
let mut mesh = egui::Mesh::default();
mesh.colored_vertex(center, col(t_at(0.0, 0.0)));
for i in 0..=n {
let a = std::f32::consts::TAU * (i as f32 / n as f32);
let (sv, cv) = a.sin_cos();
mesh.colored_vertex(center + radius * egui::vec2(cv, sv), col(t_at(cv, sv)));
}
painter.add(egui::Shape::convex_polygon(pts, c2, Stroke::NONE));
// Faint ring: light on dark, dark on light — a low-contrast gradient still
// gets an edge on either background.
for i in 1..=n as u32 {
mesh.add_triangle(0, i, i + 1);
}
painter.add(egui::Shape::mesh(mesh));
// 1px theme-aware hairline ring (matches the mock): near-black on light,
// near-white on dark, so the disc has a defined edge on either background and
// against the avatar it badges.
let ring = if t.dark_base {
Color32::from_rgba_unmultiplied(255, 255, 255, 55)
Color32::from_rgba_unmultiplied(250, 250, 247, 82)
} else {
Color32::from_rgba_unmultiplied(0, 0, 0, 45)
Color32::from_rgba_unmultiplied(14, 14, 12, 71)
};
painter.circle_stroke(center, radius, Stroke::new(1.0, ring));
}