From 6d0dde523fb4ad2f9b8a26e340b3a100d9de6842 Mon Sep 17 00:00:00 2001 From: 2ro <17595647+2ro@users.noreply.github.com> Date: Sun, 5 Jul 2026 16:46:59 -0400 Subject: [PATCH] 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. --- src/gui/views/goblin/identicon.rs | 10 +++++ src/gui/views/goblin/mod.rs | 36 +++++++-------- src/gui/views/goblin/widgets.rs | 74 ++++++++++++++++++++----------- 3 files changed, 73 insertions(+), 47 deletions(-) diff --git a/src/gui/views/goblin/identicon.rs b/src/gui/views/goblin/identicon.rs index 0fb06c24..88646146 100644 --- a/src/gui/views/goblin/identicon.rs +++ b/src/gui/views/goblin/identicon.rs @@ -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 diff --git a/src/gui/views/goblin/mod.rs b/src/gui/views/goblin/mod.rs index d02f2a46..8c0e2549 100644 --- a/src/gui/views/goblin/mod.rs +++ b/src/gui/views/goblin/mod.rs @@ -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() { diff --git a/src/gui/views/goblin/widgets.rs b/src/gui/views/goblin/widgets.rs index 2539088c..4e460d40 100644 --- a/src/gui/views/goblin/widgets.rs +++ b/src/gui/views/goblin/widgets.rs @@ -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)); }