6 Commits

Author SHA1 Message Date
818c29fca7 feat(market): settings gear, options modal & distinct portfolio styles
Add a settings-gear modal (portfolio layout / chart style / fetch prices) and persist the portfolio style. Redesign the three row styles into distinct purposes: Table (borderless grid + column header + % pill), Cards (glass card + muted label/shadow value + shielded/transparent split bar), Spotlight (heavy tile + enlarged direction-coloured hero value + delta chip). Dim empty groups. Add data::SumPortfolioSplit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 22:35:03 -05:00
0a43742897 feat(chat): side-by-side settings modal with live conversation preview
Widen the chat settings modal and split it into a live message preview (canned bubbles rendered with the real style/accent/density/font/emoji/timestamp code) beside the controls; size the Done button to its text.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 22:35:03 -05:00
defe14d913 feat(contacts): avatar shapes, list scale & live settings preview
Add a settings-gear modal with a segmented avatar-shape picker (circle / rounded square / full-row left tab), a list-scale slider, and a live two-row preview. Smooth-scroll the address list and fix a latent wheel double-scroll on the outer scroll child. (settings.h also refreshes the portfolio-style doc comment.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 22:35:03 -05:00
8b55a90102 i18n: contacts/market settings strings + portfolio-style rename
Add contacts_shape_tab, the market settings-modal + column-header keys; rename the portfolio-style labels to Table / Cards / Spotlight (same keys, new values). Rebuild the CJK subset for the new glyphs. All 8 languages, additive.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 22:35:02 -05:00
0fd3d214a4 fix(ui): clip & left-align overflowing SegmentedControl labels
material::SegmentedControl drew each label centred with no clip, so long non-English labels (Russian/German, etc.) overflowed their cell and overlapped neighbours. Left-align on overflow and clip per cell (mirroring the chat modal's segmented helper); the fits-case is visually unchanged. Benefits every caller (contacts, receive, wallets, market).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 22:35:02 -05:00
996e10ed02 fix(ui): stop forward-wheel double-scroll on peers & console tabs
The inner scroll child (##PeersList / ConsoleOutput) uses NoScrollWithMouse + ApplySmoothScroll; ImGui forwards its wheel up to the outer scroll container, so the wheel scrolled both. Add NoScrollWithMouse to the outer containers (safe: both fill the remaining height and never overflow), matching the contacts/explorer fix.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 22:35:02 -05:00
19 changed files with 826 additions and 146 deletions

Binary file not shown.

View File

@@ -375,8 +375,15 @@
"contact_preview_name": "Kontaktname",
"contact_wallet_loading": "Die Wallet lädt noch — aktiviere „In jeder Wallet anzeigen“ oder versuche es gleich erneut.",
"contacts": "Kontakte",
"contacts_avatar_shape": "Avatarform",
"contacts_list_scale": "Listengröße",
"contacts_search_no_match": "Keine passenden Kontakte",
"contacts_search_placeholder": "Kontakte durchsuchen...",
"contacts_settings_tip": "Kontakte anpassen",
"contacts_settings_title": "Kontakteinstellungen",
"contacts_shape_circle": "Kreis",
"contacts_shape_square": "Quadrat",
"contacts_shape_tab": "Reiter",
"copied": "Kopiert!",
"copy": "Kopieren",
"copy_address": "Vollständige Adresse kopieren",
@@ -723,6 +730,8 @@
"market_cap": "Marktkapitalisierung",
"market_cap_short": "Kap.",
"market_chart_loading": "Preisverlauf wird geladen",
"market_col_name": "Name",
"market_col_value": "Wert",
"market_iv_1d": "1T",
"market_iv_1h": "1S",
"market_iv_1m": "1M",
@@ -731,13 +740,18 @@
"market_no_history": "Kein Preisverlauf verfügbar",
"market_no_price": "Keine Preisdaten",
"market_now": "Jetzt",
"market_opt_chart_style": "Diagrammstil",
"market_pct_shielded": "%.0f%% Abgeschirmt",
"market_portfolio": "PORTFOLIO",
"market_price_loading": "Preisdaten werden geladen...",
"market_price_unavailable": "Preisdaten nicht verfügbar",
"market_refresh_price": "Preisdaten aktualisieren",
"market_settings_tip": "Marktoptionen",
"market_settings_title": "Markteinstellungen",
"market_style_candle": "Zu Kerzenchart wechseln",
"market_style_candle_label": "Kerzen",
"market_style_line": "Zum Liniendiagramm wechseln",
"market_style_line_label": "Linie",
"market_trade_on": "Handeln auf %s",
"market_updated": "\\xc2\\xb7 Aktualisiert %s",
"market_vol_short": "Vol.",
@@ -1041,9 +1055,9 @@
"portfolio_spark_min": "Minute",
"portfolio_spark_month": "Monat",
"portfolio_spark_week": "Woche",
"portfolio_style_compact": "Kompakte Zeilen",
"portfolio_style_detailed": "Detaillierte Zeilen",
"portfolio_style_featured": "Hervorgehobene Zeilen",
"portfolio_style_compact": "Tabelle",
"portfolio_style_detailed": "Karten",
"portfolio_style_featured": "Hervorgehoben",
"portfolio_style_label": "Portfolio-Stil",
"portfolio_untitled": "Ohne Titel",
"portfolio_wallet_loading": "Warte, bis die Wallet fertig geladen ist, um eine Gruppe hinzuzufügen.",

View File

@@ -375,8 +375,15 @@
"contact_preview_name": "Nombre del contacto",
"contact_wallet_loading": "La cartera aún se está cargando: marca «Mostrar en todas las carteras» o inténtalo de nuevo en un momento.",
"contacts": "Contactos",
"contacts_avatar_shape": "Forma del avatar",
"contacts_list_scale": "Escala de la lista",
"contacts_search_no_match": "No hay contactos coincidentes",
"contacts_search_placeholder": "Buscar contactos...",
"contacts_settings_tip": "Personalizar contactos",
"contacts_settings_title": "Ajustes de contactos",
"contacts_shape_circle": "Círculo",
"contacts_shape_square": "Cuadrado",
"contacts_shape_tab": "Pestaña",
"copied": "¡Copiado!",
"copy": "Copiar",
"copy_address": "Copiar Dirección Completa",
@@ -723,6 +730,8 @@
"market_cap": "Cap. de Mercado",
"market_cap_short": "Cap.",
"market_chart_loading": "Cargando historial de precios",
"market_col_name": "Nombre",
"market_col_value": "Valor",
"market_iv_1d": "1D",
"market_iv_1h": "1H",
"market_iv_1m": "1M",
@@ -731,13 +740,18 @@
"market_no_history": "No hay historial de precios disponible",
"market_no_price": "Sin datos de precio",
"market_now": "Ahora",
"market_opt_chart_style": "Estilo de gráfico",
"market_pct_shielded": "%.0f%% Protegido",
"market_portfolio": "PORTAFOLIO",
"market_price_loading": "Cargando datos de precio...",
"market_price_unavailable": "Datos de precio no disponibles",
"market_refresh_price": "Actualizar datos de precio",
"market_settings_tip": "Opciones de mercado",
"market_settings_title": "Ajustes de mercado",
"market_style_candle": "Cambiar a velas",
"market_style_candle_label": "Velas",
"market_style_line": "Cambiar a gráfico de líneas",
"market_style_line_label": "Línea",
"market_trade_on": "Operar en %s",
"market_updated": "\\xc2\\xb7 Actualizado %s",
"market_vol_short": "Vol",
@@ -1041,9 +1055,9 @@
"portfolio_spark_min": "Minuto",
"portfolio_spark_month": "Mes",
"portfolio_spark_week": "Semana",
"portfolio_style_compact": "Filas compactas",
"portfolio_style_detailed": "Filas detalladas",
"portfolio_style_featured": "Filas destacadas",
"portfolio_style_compact": "Tabla",
"portfolio_style_detailed": "Tarjetas",
"portfolio_style_featured": "Destacado",
"portfolio_style_label": "Estilo de cartera",
"portfolio_untitled": "Sin título",
"portfolio_wallet_loading": "Espera a que la cartera termine de cargar para añadir un grupo.",

View File

@@ -375,8 +375,15 @@
"contact_preview_name": "Nom du contact",
"contact_wallet_loading": "Le portefeuille se charge encore — cochez « Afficher dans chaque portefeuille » ou réessayez dans un instant.",
"contacts": "Contacts",
"contacts_avatar_shape": "Forme de l'avatar",
"contacts_list_scale": "Échelle de la liste",
"contacts_search_no_match": "Aucun contact correspondant",
"contacts_search_placeholder": "Rechercher des contacts...",
"contacts_settings_tip": "Personnaliser les contacts",
"contacts_settings_title": "Paramètres des contacts",
"contacts_shape_circle": "Cercle",
"contacts_shape_square": "Carré",
"contacts_shape_tab": "Onglet",
"copied": "Copié !",
"copy": "Copier",
"copy_address": "Copier l'adresse complète",
@@ -723,6 +730,8 @@
"market_cap": "Capitalisation",
"market_cap_short": "Cap.",
"market_chart_loading": "Chargement de l'historique des prix",
"market_col_name": "Nom",
"market_col_value": "Valeur",
"market_iv_1d": "1J",
"market_iv_1h": "1H",
"market_iv_1m": "1M",
@@ -731,13 +740,18 @@
"market_no_history": "Aucun historique de prix disponible",
"market_no_price": "Pas de données de prix",
"market_now": "Maintenant",
"market_opt_chart_style": "Style du graphique",
"market_pct_shielded": "%.0f%% Blindé",
"market_portfolio": "PORTEFEUILLE",
"market_price_loading": "Chargement des données de prix...",
"market_price_unavailable": "Données de prix indisponibles",
"market_refresh_price": "Actualiser les données de prix",
"market_settings_tip": "Options du marché",
"market_settings_title": "Paramètres du marché",
"market_style_candle": "Passer aux chandeliers",
"market_style_candle_label": "Chandelier",
"market_style_line": "Passer au graphique en ligne",
"market_style_line_label": "Ligne",
"market_trade_on": "Échanger sur %s",
"market_updated": "\\xc2\\xb7 Mis à jour %s",
"market_vol_short": "Vol",
@@ -1041,9 +1055,9 @@
"portfolio_spark_min": "Minute",
"portfolio_spark_month": "Mois",
"portfolio_spark_week": "Semaine",
"portfolio_style_compact": "Lignes compactes",
"portfolio_style_detailed": "Lignes détaillées",
"portfolio_style_featured": "Lignes en vedette",
"portfolio_style_compact": "Tableau",
"portfolio_style_detailed": "Cartes",
"portfolio_style_featured": "En vedette",
"portfolio_style_label": "Style du portefeuille",
"portfolio_untitled": "Sans titre",
"portfolio_wallet_loading": "Attendez la fin du chargement du portefeuille pour ajouter un groupe.",

View File

@@ -375,8 +375,15 @@
"contact_preview_name": "連絡先名",
"contact_wallet_loading": "ウォレットを読み込み中です。「すべてのウォレットに表示」にチェックするか、少し待ってから再試行してください。",
"contacts": "連絡先",
"contacts_avatar_shape": "アバターの形",
"contacts_list_scale": "リストの拡大率",
"contacts_search_no_match": "一致する連絡先がありません",
"contacts_search_placeholder": "連絡先を検索...",
"contacts_settings_tip": "連絡先のカスタマイズ",
"contacts_settings_title": "連絡先設定",
"contacts_shape_circle": "円",
"contacts_shape_square": "四角",
"contacts_shape_tab": "左タブ",
"copied": "コピーしました!",
"copy": "コピー",
"copy_address": "完全なアドレスをコピー",
@@ -723,6 +730,8 @@
"market_cap": "時価総額",
"market_cap_short": "時価総額",
"market_chart_loading": "価格履歴を読み込み中",
"market_col_name": "名前",
"market_col_value": "価値",
"market_iv_1d": "1日",
"market_iv_1h": "1時間",
"market_iv_1m": "1ヶ月",
@@ -731,13 +740,18 @@
"market_no_history": "価格履歴がありません",
"market_no_price": "価格データなし",
"market_now": "現在",
"market_opt_chart_style": "チャートスタイル",
"market_pct_shielded": "%.0f%% シールド済み",
"market_portfolio": "ポートフォリオ",
"market_price_loading": "価格データを読み込み中...",
"market_price_unavailable": "価格データが利用できません",
"market_refresh_price": "価格データを更新",
"market_settings_tip": "マーケットオプション",
"market_settings_title": "マーケット設定",
"market_style_candle": "ローソク足に切り替え",
"market_style_candle_label": "ローソク足",
"market_style_line": "折れ線チャートに切り替え",
"market_style_line_label": "ライン",
"market_trade_on": "%s で取引",
"market_updated": "\\xc2\\xb7 更新: %s",
"market_vol_short": "出来高",
@@ -1041,9 +1055,9 @@
"portfolio_spark_min": "分",
"portfolio_spark_month": "月",
"portfolio_spark_week": "週",
"portfolio_style_compact": "コンパクト行",
"portfolio_style_detailed": "詳細行",
"portfolio_style_featured": "注目行",
"portfolio_style_compact": "テーブル",
"portfolio_style_detailed": "カード",
"portfolio_style_featured": "スポットライト",
"portfolio_style_label": "ポートフォリオスタイル",
"portfolio_untitled": "無題",
"portfolio_wallet_loading": "ウォレットの読み込みが終わってからグループを追加してください。",

View File

@@ -375,8 +375,15 @@
"contact_preview_name": "연락처 이름",
"contact_wallet_loading": "지갑을 아직 불러오는 중입니다 — “모든 지갑에 표시”를 선택하거나 잠시 후 다시 시도하세요.",
"contacts": "연락처",
"contacts_avatar_shape": "아바타 모양",
"contacts_list_scale": "목록 배율",
"contacts_search_no_match": "일치하는 연락처 없음",
"contacts_search_placeholder": "연락처 검색...",
"contacts_settings_tip": "연락처 사용자 지정",
"contacts_settings_title": "연락처 설정",
"contacts_shape_circle": "원",
"contacts_shape_square": "사각형",
"contacts_shape_tab": "왼쪽 탭",
"copied": "복사됨!",
"copy": "복사",
"copy_address": "전체 주소 복사",
@@ -723,6 +730,8 @@
"market_cap": "시가총액",
"market_cap_short": "시총",
"market_chart_loading": "가격 기록 불러오는 중",
"market_col_name": "이름",
"market_col_value": "가치",
"market_iv_1d": "1일",
"market_iv_1h": "1시간",
"market_iv_1m": "1개월",
@@ -731,13 +740,18 @@
"market_no_history": "가격 내역 없음",
"market_no_price": "가격 데이터 없음",
"market_now": "현재",
"market_opt_chart_style": "차트 스타일",
"market_pct_shielded": "%.0f%% 차폐됨",
"market_portfolio": "포트폴리오",
"market_price_loading": "가격 데이터를 불러오는 중...",
"market_price_unavailable": "가격 데이터를 사용할 수 없습니다",
"market_refresh_price": "가격 데이터 새로고침",
"market_settings_tip": "마켓 옵션",
"market_settings_title": "마켓 설정",
"market_style_candle": "캔들차트로 전환",
"market_style_candle_label": "캔들",
"market_style_line": "선형 차트로 전환",
"market_style_line_label": "라인",
"market_trade_on": "%s에서 거래",
"market_updated": "\\xc2\\xb7 업데이트됨 %s",
"market_vol_short": "거래량",
@@ -1041,9 +1055,9 @@
"portfolio_spark_min": "분",
"portfolio_spark_month": "월",
"portfolio_spark_week": "주",
"portfolio_style_compact": "간결한 행",
"portfolio_style_detailed": "상세 행",
"portfolio_style_featured": "강조 행",
"portfolio_style_compact": "테이블",
"portfolio_style_detailed": "카드",
"portfolio_style_featured": "스포트라이트",
"portfolio_style_label": "포트폴리오 스타일",
"portfolio_untitled": "제목 없음",
"portfolio_wallet_loading": "지갑 로딩이 끝난 후 그룹을 추가하세요.",

View File

@@ -375,8 +375,15 @@
"contact_preview_name": "Nome do contato",
"contact_wallet_loading": "A carteira ainda está carregando — marque “Mostrar em todas as carteiras” ou tente novamente em um momento.",
"contacts": "Contatos",
"contacts_avatar_shape": "Forma do avatar",
"contacts_list_scale": "Escala da lista",
"contacts_search_no_match": "Nenhum contato correspondente",
"contacts_search_placeholder": "Pesquisar contatos...",
"contacts_settings_tip": "Personalizar contatos",
"contacts_settings_title": "Configurações de contatos",
"contacts_shape_circle": "Círculo",
"contacts_shape_square": "Quadrado",
"contacts_shape_tab": "Aba",
"copied": "Copiado!",
"copy": "Copiar",
"copy_address": "Copiar Endereço Completo",
@@ -723,6 +730,8 @@
"market_cap": "Capitalização",
"market_cap_short": "Cap.",
"market_chart_loading": "Carregando histórico de preços",
"market_col_name": "Nome",
"market_col_value": "Valor",
"market_iv_1d": "1D",
"market_iv_1h": "1H",
"market_iv_1m": "1M",
@@ -731,13 +740,18 @@
"market_no_history": "Nenhum histórico de preços disponível",
"market_no_price": "Sem dados de preço",
"market_now": "Agora",
"market_opt_chart_style": "Estilo do gráfico",
"market_pct_shielded": "%.0f%% Blindado",
"market_portfolio": "PORTFÓLIO",
"market_price_loading": "Carregando dados de preço...",
"market_price_unavailable": "Dados de preço indisponíveis",
"market_refresh_price": "Atualizar dados de preço",
"market_settings_tip": "Opções de mercado",
"market_settings_title": "Configurações de mercado",
"market_style_candle": "Mudar para velas",
"market_style_candle_label": "Velas",
"market_style_line": "Mudar para gráfico de linhas",
"market_style_line_label": "Linha",
"market_trade_on": "Negociar no %s",
"market_updated": "\\xc2\\xb7 Atualizado %s",
"market_vol_short": "Vol",
@@ -1041,9 +1055,9 @@
"portfolio_spark_min": "Minuto",
"portfolio_spark_month": "Mês",
"portfolio_spark_week": "Semana",
"portfolio_style_compact": "Linhas compactas",
"portfolio_style_detailed": "Linhas detalhadas",
"portfolio_style_featured": "Linhas em destaque",
"portfolio_style_compact": "Tabela",
"portfolio_style_detailed": "Cartões",
"portfolio_style_featured": "Destaque",
"portfolio_style_label": "Estilo do portfólio",
"portfolio_untitled": "Sem título",
"portfolio_wallet_loading": "Aguarde a carteira terminar de carregar para adicionar um grupo.",

View File

@@ -375,8 +375,15 @@
"contact_preview_name": "Имя контакта",
"contact_wallet_loading": "Кошелёк ещё загружается — отметьте «Показывать во всех кошельках» или повторите чуть позже.",
"contacts": "Контакты",
"contacts_avatar_shape": "Форма аватара",
"contacts_list_scale": "Масштаб списка",
"contacts_search_no_match": "Совпадающих контактов нет",
"contacts_search_placeholder": "Поиск контактов...",
"contacts_settings_tip": "Настройка контактов",
"contacts_settings_title": "Настройки контактов",
"contacts_shape_circle": "Круг",
"contacts_shape_square": "Квадрат",
"contacts_shape_tab": "Вкладка",
"copied": "Скопировано!",
"copy": "Копировать",
"copy_address": "Копировать полный адрес",
@@ -723,6 +730,8 @@
"market_cap": "Рыночная капитализация",
"market_cap_short": "Кап.",
"market_chart_loading": "Загрузка истории цен",
"market_col_name": "Название",
"market_col_value": "Стоимость",
"market_iv_1d": "1Д",
"market_iv_1h": "1Ч",
"market_iv_1m": "1М",
@@ -731,13 +740,18 @@
"market_no_history": "Нет истории цен",
"market_no_price": "Нет данных о ценах",
"market_now": "Сейчас",
"market_opt_chart_style": "Стиль графика",
"market_pct_shielded": "%.0f%% Экранировано",
"market_portfolio": "ПОРТФЕЛЬ",
"market_price_loading": "Загрузка данных о ценах...",
"market_price_unavailable": "Данные о ценах недоступны",
"market_refresh_price": "Обновить данные о ценах",
"market_settings_tip": "Параметры рынка",
"market_settings_title": "Настройки рынка",
"market_style_candle": "Переключить на свечи",
"market_style_candle_label": "Свечи",
"market_style_line": "Переключить на линейный график",
"market_style_line_label": "Линия",
"market_trade_on": "Торговать на %s",
"market_updated": "\\xc2\\xb7 Обновлено %s",
"market_vol_short": "Объём",
@@ -1041,9 +1055,9 @@
"portfolio_spark_min": "Минута",
"portfolio_spark_month": "Месяц",
"portfolio_spark_week": "Неделя",
"portfolio_style_compact": "Компактные строки",
"portfolio_style_detailed": "Подробные строки",
"portfolio_style_featured": "Избранные строки",
"portfolio_style_compact": "Таблица",
"portfolio_style_detailed": "Карточки",
"portfolio_style_featured": "Витрина",
"portfolio_style_label": "Стиль портфеля",
"portfolio_untitled": "Без названия",
"portfolio_wallet_loading": "Дождитесь загрузки кошелька, чтобы добавить группу.",

View File

@@ -375,8 +375,15 @@
"contact_preview_name": "联系人名称",
"contact_wallet_loading": "钱包仍在加载——请选中“在每个钱包中显示”,或稍后再试。",
"contacts": "联系人",
"contacts_avatar_shape": "头像形状",
"contacts_list_scale": "列表缩放",
"contacts_search_no_match": "没有匹配的联系人",
"contacts_search_placeholder": "搜索联系人...",
"contacts_settings_tip": "联系人自定义",
"contacts_settings_title": "联系人设置",
"contacts_shape_circle": "圆形",
"contacts_shape_square": "方形",
"contacts_shape_tab": "左标签",
"copied": "已复制!",
"copy": "复制",
"copy_address": "复制完整地址",
@@ -723,6 +730,8 @@
"market_cap": "市值",
"market_cap_short": "市值",
"market_chart_loading": "正在加载价格历史",
"market_col_name": "名称",
"market_col_value": "价值",
"market_iv_1d": "1天",
"market_iv_1h": "1时",
"market_iv_1m": "1M",
@@ -731,13 +740,18 @@
"market_no_history": "无价格历史",
"market_no_price": "无价格数据",
"market_now": "现在",
"market_opt_chart_style": "图表样式",
"market_pct_shielded": "%.0f%% 屏蔽",
"market_portfolio": "投资组合",
"market_price_loading": "正在加载价格数据...",
"market_price_unavailable": "价格数据不可用",
"market_refresh_price": "刷新价格数据",
"market_settings_tip": "市场选项",
"market_settings_title": "市场设置",
"market_style_candle": "切换到蜡烛图",
"market_style_candle_label": "K线",
"market_style_line": "切换到折线图",
"market_style_line_label": "折线",
"market_trade_on": "在 %s 交易",
"market_updated": "\\xc2\\xb7 已更新 %s",
"market_vol_short": "成交量",
@@ -1041,9 +1055,9 @@
"portfolio_spark_min": "分钟",
"portfolio_spark_month": "月",
"portfolio_spark_week": "周",
"portfolio_style_compact": "紧凑行",
"portfolio_style_detailed": "详细行",
"portfolio_style_featured": "特色行",
"portfolio_style_compact": "表格",
"portfolio_style_detailed": "卡片",
"portfolio_style_featured": "聚焦",
"portfolio_style_label": "投资组合样式",
"portfolio_untitled": "未命名",
"portfolio_wallet_loading": "请等待钱包加载完成后再添加分组。",

View File

@@ -196,6 +196,8 @@ bool Settings::load(const std::string& path)
if (portfolio_style_ < 0 || portfolio_style_ > 2) portfolio_style_ = 0;
loadScalar(j, "contacts_view_mode", contacts_view_mode_);
if (contacts_view_mode_ < 0 || contacts_view_mode_ > 2) contacts_view_mode_ = 0;
loadScalar(j, "contacts_avatar_shape", contacts_avatar_shape_); setContactsAvatarShape(contacts_avatar_shape_);
loadScalar(j, "contacts_list_scale", contacts_list_scale_); setContactsListScale(contacts_list_scale_);
loadScalar(j, "animate_avatars", animate_avatars_);
loadScalar(j, "scanline_enabled", scanline_enabled_);
loadScalar(j, "console_line_accents", console_line_accents_);
@@ -467,6 +469,8 @@ bool Settings::save(const std::string& path)
j["balance_layout"] = balance_layout_; // saved as string ID
j["portfolio_style"] = portfolio_style_;
j["contacts_view_mode"] = contacts_view_mode_;
j["contacts_avatar_shape"] = contacts_avatar_shape_;
j["contacts_list_scale"] = contacts_list_scale_;
j["animate_avatars"] = animate_avatars_;
j["scanline_enabled"] = scanline_enabled_;
j["console_line_accents"] = console_line_accents_;

View File

@@ -226,13 +226,19 @@ public:
std::string getBalanceLayout() const { return balance_layout_; }
void setBalanceLayout(const std::string& v) { balance_layout_ = v; }
// Market-tab portfolio row style: 0 = single-line, 1 = two-line, 2 = value-hero. Cycled with
// Left/Right arrows on the Market tab (like the Overview layouts).
// Market-tab portfolio row style: 0 = Table (borderless grid), 1 = Cards (glass card + Z/T bar),
// 2 = Spotlight (hero value). Cycled with Left/Right arrows or set in the Market settings modal.
int getPortfolioStyle() const { return portfolio_style_; }
void setPortfolioStyle(int v) { portfolio_style_ = (v < 0 || v > 2) ? 0 : v; }
// Contacts tab address-list view: 0 = cards, 1 = list, 2 = table.
int getContactsViewMode() const { return contacts_view_mode_; }
void setContactsViewMode(int v) { contacts_view_mode_ = (v < 0 || v > 2) ? 0 : v; }
// Contacts customization (gear modal): avatar shape + card/list row scale.
// Avatar shape: 0 = circle, 1 = rounded square, 2 = full-row-height left tab (rounded-left, flat right).
int getContactsAvatarShape() const { return contacts_avatar_shape_; }
void setContactsAvatarShape(int v) { contacts_avatar_shape_ = (v < 0 || v > 2) ? 0 : v; }
float getContactsListScale() const { return contacts_list_scale_; }
void setContactsListScale(float v) { contacts_list_scale_ = std::max(0.8f, std::min(1.5f, v)); }
// Play animated contact avatars (GIF/WebP). Off = show the first frame only.
bool getAnimateAvatars() const { return animate_avatars_; }
void setAnimateAvatars(bool v) { animate_avatars_ = v; }
@@ -553,8 +559,10 @@ private:
float window_opacity_ = 1.0f; // Mac/Linux: default fully opaque
#endif
std::string balance_layout_ = "classic";
int portfolio_style_ = 0; // Market portfolio row style (0 single / 1 two-line / 2 hero)
int portfolio_style_ = 0; // Market portfolio row style (0 Table / 1 Cards / 2 Spotlight)
int contacts_view_mode_ = 0; // Contacts address-list view (0 cards / 1 list / 2 table)
int contacts_avatar_shape_ = 0; // 0 = circle, 1 = rounded square, 2 = full-height left tab
float contacts_list_scale_ = 1.0f; // card/list row scale (does not affect the table view)
bool animate_avatars_ = true; // play animated (GIF/WebP) contact avatars
bool scanline_enabled_ = true;
bool console_line_accents_ = true; // left color accent bars in console output

View File

@@ -31,6 +31,19 @@ inline double SumPortfolioBalance(const std::vector<std::string>& entryAddresses
return sum;
}
// Shielded/transparent breakdown of an entry's balance (for the per-group Z/T split bar). Pure.
struct PortfolioSplit { double shielded = 0.0; double transparent = 0.0; };
inline PortfolioSplit SumPortfolioSplit(const std::vector<std::string>& entryAddresses,
const std::vector<AddressInfo>& walletAddresses)
{
PortfolioSplit s;
if (entryAddresses.empty()) return s;
std::unordered_set<std::string> want(entryAddresses.begin(), entryAddresses.end());
for (const auto& a : walletAddresses)
if (want.count(a.address)) { if (a.isShielded()) s.shielded += a.balance; else s.transparent += a.balance; }
return s;
}
// Whether `address` is part of the entry's address group.
inline bool PortfolioEntryContains(const std::vector<std::string>& entryAddresses,
const std::string& address)

View File

@@ -1347,10 +1347,17 @@ inline int SegmentedControl(ImDrawList* dl, ImVec2 origin, float totalW, float h
ImVec2(cMax.x - 2.0f * dp, cMax.y - 2.0f * dp),
WithAlpha(Primary(), 210), (height - 4.0f * dp) * 0.5f);
ImVec2 ts = font->CalcTextSizeA(font->LegacySize, FLT_MAX, 0, labels[i]);
// Center when the label fits; otherwise left-align with a small pad (so a long translation clips
// on the right, not on BOTH sides). Clip to the cell so no label can bleed into a neighbouring
// segment or past the rounded track — English fits, but de/es/fr/pt/ru labels can overrun.
const float lpad = 4.0f * dp;
const float tx = (ts.x <= cellW - 2.0f * lpad) ? (cellW - ts.x) * 0.5f : lpad;
dl->PushClipRect(cMin, cMax, true);
dl->AddText(font, font->LegacySize,
ImVec2(cMin.x + (cellW - ts.x) * 0.5f, cMin.y + (height - ts.y) * 0.5f),
ImVec2(cMin.x + tx, cMin.y + (height - ts.y) * 0.5f),
active ? IM_COL32(255, 255, 255, 255) : (hov ? OnSurface() : OnSurfaceMedium()),
labels[i]);
dl->PopClipRect();
ImGui::PushID(i);
ImGui::SetCursorScreenPos(cMin);
if (ImGui::InvisibleButton(idBase, ImVec2(cellW, height))) clicked = i;

View File

@@ -191,6 +191,121 @@ std::string initialOf(const std::string& name) {
return name.substr(i, std::min(len, name.size() - i));
}
// Live "conversation preview" for the chat settings modal. Renders a few canned bubbles with the SAME
// style / accent / fill / rounding / density / font-size / timestamp / emoji code the real message loop
// uses (see the loop in RenderChatTab), so the modal shows exactly what chat will look like. Purely
// presentational — no hover/click handlers. Reserves a `width`-wide, content-tall block via a Dummy.
void RenderChatSettingsPreview(App* app, float width) {
auto* cs = app ? app->settings() : nullptr;
if (!cs) return;
const float dp = Layout::dpiScale();
ImDrawList* dl = ImGui::GetWindowDrawList();
// ---- Settings-derived draw params (mirror the real loop's formulas exactly). ----
const int bubbleStyle = cs->getChatBubbleStyle(); // 0 rounded, 1 square, 2 minimal
const bool compact = cs->getChatDensity() == 1;
const float fscale = cs->getChatFontScale();
ImFont* nameFont = material::Type().body2();
ImFont* metaFont = material::Type().caption();
const float nameSz = scaledSize(nameFont);
const float metaSz = scaledSize(metaFont);
const float bodySz = nameSz * (18.0f / std::max(1.0f, Layout::kFontBody2())) * fscale;
const float bpad = (compact ? 6.0f : 9.0f) * dp;
const float bround = (bubbleStyle == 1 ? 2.0f : 9.0f) * dp;
const int outFill = (bubbleStyle == 2 ? 26 : 46);
const int inFill = (bubbleStyle == 2 ? 14 : 22);
const float groupGap = (compact ? 4.0f : 7.0f) * dp;
const float msgGap = (compact ? 2.0f : 3.0f) * dp;
const ImU32 accentBase = bubbleAccentColor(cs->getChatBubbleAccent());
const float avR = 11.0f * dp;
const float gutter = 2.0f * avR + 6.0f * dp;
// Resolve the chat clock the same way line ~379 does, but locally so the preview reflects the
// timestamp control the SAME frame it changes (0 = follow global, 1 = 24h, 2 = 12h).
const int ctf = cs->getChatTimeFormat();
const bool prev12h = (ctf == 2) || (ctf == 0 && cs->getTimeFormat() == 1);
const char* t1 = prev12h ? "9:41 AM" : "09:41";
const char* t2 = prev12h ? "9:42 AM" : "09:42";
// Canned conversation: one incoming bubble + a grouped outgoing pair (to show the flattened corner
// of the rounded/minimal styles). Emoji in the bodies show the mono/color emoji style.
struct PMsg { const char* body; bool outgoing; bool startGroup; bool lastInGroup; std::string meta; };
const std::string peer = "Ava";
const PMsg msgs[] = {
{ u8"Did the payment go through? \U0001F642", false, true, true, peer + " " + t1 },
{ u8"Yep — just confirmed ✅", true, true, false, std::string(TR("chat_you")) + " " + t2 },
{ u8"Sending the rest now \U0001F44D", true, false, true, std::string() },
};
const int N = 3;
// Panel geometry + per-message layout (pre-measured so the glass panel is drawn behind at the exact
// height, since the draw list has no retained ordering).
const float padIn = 10.0f * dp;
const float contentW = std::max(80.0f * dp, width - 2.0f * padIn);
const float maxBubbleW = std::max(120.0f * dp, contentW * 0.80f);
const float innerW = maxBubbleW - 2.0f * bpad;
struct PLay { ImVec2 tsz; float bw; float bh; };
PLay lay[3];
float totalH = padIn;
for (int i = 0; i < N; ++i) {
if (msgs[i].startGroup) totalH += (i > 0 ? groupGap : 0.0f) + metaSz + 3.0f * dp;
else totalH += msgGap;
lay[i].tsz = nameFont->CalcTextSizeA(bodySz, innerW, innerW, msgs[i].body);
lay[i].bw = std::min(maxBubbleW, lay[i].tsz.x + 2.0f * bpad);
lay[i].bh = lay[i].tsz.y + 2.0f * bpad;
totalH += lay[i].bh;
}
totalH += padIn;
const ImVec2 origin = ImGui::GetCursorScreenPos();
material::GlassPanelSpec g; g.rounding = 10.0f * dp; g.fillAlpha = 16; g.borderAlpha = 36;
material::DrawGlassPanel(dl, origin, ImVec2(origin.x + width, origin.y + totalH), g);
const float leftX = origin.x + padIn;
const float rowW = contentW;
float cy = origin.y + padIn;
for (int i = 0; i < N; ++i) {
const bool outgoing = msgs[i].outgoing;
if (msgs[i].startGroup) {
if (i > 0) cy += groupGap;
const ImVec2 msz = metaFont->CalcTextSizeA(metaSz, FLT_MAX, 0.0f, msgs[i].meta.c_str());
const float mtx = outgoing ? (leftX + rowW - msz.x) : (leftX + gutter);
dl->AddText(metaFont, metaSz, ImVec2(mtx, cy), material::OnSurfaceMedium(), msgs[i].meta.c_str());
cy += metaSz + 3.0f * dp;
} else {
cy += msgGap;
}
const float bw = lay[i].bw, bh = lay[i].bh;
const float bx = outgoing ? (leftX + rowW - bw) : (leftX + gutter);
const ImVec2 bmin(bx, cy), bmax(bx + bw, cy + bh);
const ImU32 bubCol = outgoing ? material::WithAlpha(accentBase, outFill)
: material::WithAlpha(material::OnSurface(), inFill);
ImDrawFlags rf = ImDrawFlags_RoundCornersAll;
if (bubbleStyle != 1) { // square keeps uniform corners; rounded/minimal flatten stacked corners
if (outgoing) {
if (!msgs[i].startGroup) rf &= ~ImDrawFlags_RoundCornersTopRight;
if (!msgs[i].lastInGroup) rf &= ~ImDrawFlags_RoundCornersBottomRight;
} else {
if (!msgs[i].startGroup) rf &= ~ImDrawFlags_RoundCornersTopLeft;
if (!msgs[i].lastInGroup) rf &= ~ImDrawFlags_RoundCornersBottomLeft;
}
}
dl->AddRectFilled(bmin, bmax, bubCol, bround, rf);
dl->AddText(nameFont, bodySz, ImVec2(bx + bpad, cy + bpad), material::OnSurface(),
msgs[i].body, nullptr, innerW);
if (!outgoing && msgs[i].lastInGroup) { // peer avatar beside the last incoming bubble of the run
const ImVec2 avc(leftX + avR, cy + bh - avR);
dl->AddCircleFilled(avc, avR, avatarColor("preview:" + peer), 20);
const std::string init = initialOf(peer);
const ImVec2 isz = metaFont->CalcTextSizeA(metaSz, FLT_MAX, 0.0f, init.c_str());
dl->AddText(metaFont, metaSz, ImVec2(avc.x - isz.x * 0.5f, avc.y - isz.y * 0.5f),
IM_COL32(255, 255, 255, 235), init.c_str());
}
cy += bh;
}
ImGui::Dummy(ImVec2(width, totalH));
}
// Render a peer identity key (hex) as a groupable fingerprint for the verify tooltip: uppercased,
// spaced every 4 hex chars, wrapped every 16 (8 bytes) so two users can read it off line by line.
std::string keyFingerprint(const std::string& hex) {
@@ -1195,16 +1310,43 @@ void RenderChatTab(App* app)
ov.title = TR("chat_settings_title");
ov.p_open = &s_show_chat_settings; // X / backdrop closes it
ov.style = material::OverlayStyle::BlurFloat;
ov.cardWidth = 520.0f; ov.idSuffix = "chatsettings";
// Wider than tall: a live preview sits to the LEFT of the controls (more horizontal room to work
// with than vertical). Both columns AutoResizeY so neither the preview nor the controls clip.
ov.cardWidth = 800.0f; ov.idSuffix = "chatsettings";
if (material::BeginOverlayDialog(ov)) {
RenderChatSettingsControls(app);
ImGui::Dummy(ImVec2(0.0f, 8.0f * Layout::dpiScale()));
const float dp = Layout::dpiScale();
const float colGap = 16.0f * dp;
const float fullW = ImGui::GetContentRegionAvail().x;
const float previewW = std::min(340.0f * dp, std::max(240.0f * dp, fullW * 0.40f));
const float controlsW = std::max(300.0f * dp, fullW - previewW - colGap);
ImGui::BeginChild("##chatPreviewCol", ImVec2(previewW, 0.0f), ImGuiChildFlags_AutoResizeY,
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
RenderChatSettingsPreview(app, ImGui::GetContentRegionAvail().x);
ImGui::EndChild();
ImGui::SameLine(0.0f, colGap);
ImGui::BeginChild("##chatControlsCol", ImVec2(controlsW, 0.0f), ImGuiChildFlags_AutoResizeY,
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
RenderChatSettingsControls(app, 0.0f);
ImGui::EndChild();
ImGui::Dummy(ImVec2(0.0f, 8.0f * dp));
{
// Done: sized to its text (+ a little shoulder room) and centered, not full width.
const char* doneLbl = TR("chat_settings_done");
ImGui::PushFont(material::Type().button());
const float bw = ImGui::CalcTextSize(doneLbl).x + ImGui::GetStyle().FramePadding.x * 2.0f + 28.0f * dp;
ImGui::PopFont();
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + std::max(0.0f, (ImGui::GetContentRegionAvail().x - bw) * 0.5f));
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(material::WithAlpha(material::Primary(), 205)));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(material::Primary()));
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImGui::ColorConvertU32ToFloat4(material::WithAlpha(material::Primary(), 235)));
if (material::TactileButton(TR("chat_settings_done"), ImVec2(ImGui::GetContentRegionAvail().x, 0)))
if (material::TactileButton(doneLbl, ImVec2(bw, 0)))
s_show_chat_settings = false;
ImGui::PopStyleColor(3);
}
material::EndOverlayDialog();
}
}

View File

@@ -311,8 +311,12 @@ void ConsoleTab::render(ConsoleCommandExecutor& exec)
computeVisibleLines(has_text_filter_, filter_lower_);
// Main console layout
// NoScrollWithMouse: the inner ConsoleOutput owns wheel scrolling (via ApplySmoothScroll). Without
// this, a wheel over the output would scroll BOTH the output (smooth-scroll) and this outer container
// (ImGui forwards the NoScrollWithMouse child's wheel to its scrollable ancestor) — a double-scroll.
// Safe because the output panel is sized to fill the remaining height, so this outer never overflows.
ImGui::BeginChild("ConsoleContainer", ImVec2(0, 0), false,
ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollbar);
ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
// Optional status header (lite: sync + last error) above the toolbar.
renderStatusHeader(exec);

View File

@@ -37,6 +37,7 @@ namespace ui {
static int s_selected_index = -1;
static bool s_show_add_dialog = false;
static bool s_show_edit_dialog = false;
static bool s_show_contacts_settings = false; // Contacts customization modal (gear button)
static bool s_focus_edit_field = false; // focus the first field the frame the add/edit dialog opens
static int s_confirm_delete_idx = -1; // armed storage index; a 2nd Delete confirms
static char s_edit_label[128] = "";
@@ -81,6 +82,8 @@ struct AvatarTex {
static std::unordered_map<std::string, AvatarTex> s_avatarTexCache;
static int s_avatarLoadsThisFrame = 0;
static bool s_animateAvatars = true; // mirrors the setting; refreshed each frame in RenderContactsTab
static int s_contactsShape = 0; // avatar shape (mirrors getContactsAvatarShape: 0/1/2)
static float s_contactsScale = 1.0f; // card/list row scale (mirrors getContactsListScale)
static bool s_avatarAnimatedThisFrame = false; // set when a live animated frame is drawn -> keep redrawing
static constexpr int kAvatarLoadsPerFrame = 2;
static constexpr int kAvatarMaxFrames = 300; // cap frames per animated avatar (bounds VRAM/decode)
@@ -134,22 +137,48 @@ static void invalidateAvatarTexture(const std::string& path) {
s_avatarTexCache.erase(it);
}
// Draw a contact's avatar into the circle at `c` (radius `r`): a custom image (circular-cropped), a
// Material icon in a tinted circle, or — the default — the Z/T type badge.
// Draw a contact's avatar: a custom image, a Material icon in a tinted shape, or the Z/T type badge.
// `shape` picks the outline: 0 = circle (centered at `c`, radius `r`); 1 = rounded square (same box as
// the circle, all corners rounded); 2 = a full-row-height tab spanning [tabMin, tabMax] with rounded
// LEFT corners (radius tabRound) + a straight right edge. The inner glyph is sized to the shape, so it
// stays proportional at any list scale.
static void drawContactAvatar(ImDrawList* dl, ImVec2 c, float r, const data::AddressBookEntry& e,
bool shielded, ImU32 typeCol, bool light, float dp,
ImFont* letterFont, ImFont* iconFont, bool onScreen = true) {
ImFont* letterFont, ImFont* iconFont, bool onScreen = true, int shape = 0,
ImVec2 tabMin = ImVec2(0, 0), ImVec2 tabMax = ImVec2(0, 0), float tabRound = 0.0f) {
const bool circle = (shape == 0);
// Resolve the shape rectangle, its rounding, the content center + radius, and the glyph size.
ImVec2 cc = c; float cr = r;
ImVec2 sMin(c.x - r, c.y - r), sMax(c.x + r, c.y + r);
float sRound = r * 0.42f;
ImDrawFlags sFlags = ImDrawFlags_RoundCornersAll;
if (shape == 2) {
sMin = tabMin; sMax = tabMax; sRound = tabRound; sFlags = ImDrawFlags_RoundCornersLeft;
cc = ImVec2((tabMin.x + tabMax.x) * 0.5f, (tabMin.y + tabMax.y) * 0.5f);
cr = (tabMax.y - tabMin.y) * 0.5f;
}
const float glyphSz = cr * 0.92f; // ~46% of the shape diameter — matches the classic badge ratio
auto fillShape = [&](ImU32 col) {
if (circle) dl->AddCircleFilled(cc, cr, col);
else dl->AddRectFilled(sMin, sMax, col, sRound, sFlags);
};
auto borderShape = [&](ImU32 col, float th) {
if (circle) dl->AddCircle(cc, cr, col, 0, th);
else dl->AddRect(sMin, sMax, col, sRound, sFlags, th);
};
const std::string& av = e.avatar;
if (av.rfind("img:", 0) == 0) {
const AvatarTex* t = getAvatarTexture(av.substr(4));
ImTextureID tex = currentAvatarFrame(t, onScreen);
if (tex) {
float u0 = 0, v0 = 0, u1 = 1, v1 = 1; // centre-crop to a square so the circle isn't stretched
if (t->w > t->h) { float m = (t->w - t->h) * 0.5f / t->w; u0 = m; u1 = 1 - m; }
else if (t->h > t->w) { float m = (t->h - t->w) * 0.5f / t->h; v0 = m; v1 = 1 - m; }
dl->AddImageRounded(tex, ImVec2(c.x - r, c.y - r), ImVec2(c.x + r, c.y + r),
ImVec2(u0, v0), ImVec2(u1, v1), IM_COL32_WHITE, r);
dl->AddCircle(c, r, material::WithAlpha(material::OnSurface(), 45), 0, 1.0f * dp);
const float aspect = (sMax.x - sMin.x) / std::max(1.0f, sMax.y - sMin.y); // crop to the shape's aspect
float u0 = 0, v0 = 0, u1 = 1, v1 = 1;
const float tw = static_cast<float>(t->w), th2 = static_cast<float>(t->h);
if (tw / th2 > aspect) { float keep = aspect * th2 / tw; float m = (1 - keep) * 0.5f; u0 = m; u1 = 1 - m; }
else { float keep = (tw / aspect) / th2; float m = (1 - keep) * 0.5f; v0 = m; v1 = 1 - m; }
dl->AddImageRounded(tex, sMin, sMax, ImVec2(u0, v0), ImVec2(u1, v1), IM_COL32_WHITE,
circle ? cr : sRound, circle ? ImDrawFlags_RoundCornersAll : sFlags);
borderShape(material::WithAlpha(material::OnSurface(), 45), 1.0f * dp);
return;
}
// fall through to the type badge if the image failed to load
@@ -158,19 +187,19 @@ static void drawContactAvatar(ImDrawList* dl, ImVec2 c, float r, const data::Add
const bool known = (iconName == material::project_icons::kPickaxeName) ||
(material::project_icons::glyphForName(iconName) != nullptr);
if (known) {
dl->AddCircleFilled(c, r, material::WithAlpha(typeCol, light ? 45 : 60));
dl->AddCircle(c, r, material::WithAlpha(typeCol, 190), 0, 1.4f * dp);
material::project_icons::drawByName(dl, iconName, c, typeCol, iconFont, iconFont->LegacySize);
fillShape(material::WithAlpha(typeCol, light ? 45 : 60));
borderShape(material::WithAlpha(typeCol, 190), 1.4f * dp);
material::project_icons::drawByName(dl, iconName, cc, typeCol, iconFont, glyphSz);
return;
}
// fall through to the type badge if the icon name is unknown
}
// Default: Z/T type badge.
dl->AddCircleFilled(c, r, material::WithAlpha(typeCol, light ? 45 : 60));
dl->AddCircle(c, r, material::WithAlpha(typeCol, 190), 0, 1.4f * dp);
fillShape(material::WithAlpha(typeCol, light ? 45 : 60));
borderShape(material::WithAlpha(typeCol, 190), 1.4f * dp);
const char* letter = shielded ? "Z" : "T";
const ImVec2 ls = letterFont->CalcTextSizeA(letterFont->LegacySize, FLT_MAX, 0, letter);
dl->AddText(letterFont, letterFont->LegacySize, ImVec2(c.x - ls.x * 0.5f, c.y - ls.y * 0.5f), typeCol, letter);
const ImVec2 ls = letterFont->CalcTextSizeA(glyphSz, FLT_MAX, 0, letter);
dl->AddText(letterFont, glyphSz, ImVec2(cc.x - ls.x * 0.5f, cc.y - ls.y * 0.5f), typeCol, letter);
}
static bool isShieldedAddr(const std::string& a) {
@@ -268,6 +297,8 @@ void RenderContactsTab(App* app)
{
s_avatarLoadsThisFrame = 0; // reset the per-frame avatar-decode budget (see getAvatarTexture)
s_animateAvatars = !app->settings() || app->settings()->getAnimateAvatars();
s_contactsShape = app->settings() ? app->settings()->getContactsAvatarShape() : 0;
s_contactsScale = app->settings() ? app->settings()->getContactsListScale() : 1.0f;
auto& S = schema::UI();
// Reuse the existing address-book schema/column config for the table + add/edit form.
auto addrTable = S.table("dialogs.address-book", "address-table");
@@ -416,9 +447,12 @@ void RenderContactsTab(App* app)
const bool sh = isShieldedAddr(s_edit_address);
const ImU32 tu = contactTypeColor(sh, light);
ImVec2 avC(pMin.x + pad + avR, pMin.y + ph * 0.5f);
drawContactAvatar(pdl, avC, avR, pv, sh, tu, light, dp, letF, icoF);
const float pTabW = ph; // full preview-height square tab (shape 2)
drawContactAvatar(pdl, avC, avR, pv, sh, tu, light, dp, letF, icoF, true,
s_contactsShape, pMin, ImVec2(pMin.x + pTabW, pMax.y), 10.0f * dp);
float tx = avC.x + avR + Layout::spacingMd();
float tx = (s_contactsShape == 2) ? (pMin.x + pTabW + Layout::spacingMd())
: (avC.x + avR + Layout::spacingMd());
float textRight = pMax.x - pad;
float ty = pMin.y + (ph - blockH) * 0.5f;
const bool hasName = s_edit_label[0] != '\0';
@@ -819,7 +853,8 @@ void RenderContactsTab(App* app)
// Inline tab content lives in a scroll child (mirrors peers_tab / explorer_tab).
ImVec2 avail = ImGui::GetContentRegionAvail();
ImGui::BeginChild("##ContactsScroll", avail, false,
ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollbar);
ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollbar |
ImGuiWindowFlags_NoScrollWithMouse); // inner ##contactList owns the wheel (smooth scroll)
// Toolbar — icon + label actions; the primary "Add New" is accented so it stands out.
const float dp = ui::Layout::dpiScale();
@@ -885,8 +920,10 @@ void RenderContactsTab(App* app)
const char* segIcons[3] = { ICON_MD_VIEW_AGENDA, ICON_MD_VIEW_LIST, ICON_MD_TABLE_ROWS };
const float segH = ImGui::GetFrameHeight();
const float segW = 44.0f * dp * 3.0f;
const float segGap = 6.0f * dp;
const float grpW = segW + segGap + segH; // view toggle + gap + a square settings gear
ImGui::SameLine();
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + std::max(0.0f, ImGui::GetContentRegionAvail().x - segW));
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + std::max(0.0f, ImGui::GetContentRegionAvail().x - grpW));
ImVec2 segOrigin = ImGui::GetCursorScreenPos();
int segClk = material::SegmentedControl(ImGui::GetWindowDrawList(), segOrigin, segW, segH,
segIcons, 3, viewMode, material::Type().iconSmall(),
@@ -896,11 +933,21 @@ void RenderContactsTab(App* app)
app->settings()->save();
viewMode = segClk;
}
ImGui::Dummy(ImVec2(segW, segH));
// Contacts-customization gear, immediately right of the view toggle.
ImGui::SetCursorScreenPos(ImVec2(segOrigin.x + segW + segGap, segOrigin.y));
material::IconButtonStyle gst;
gst.tooltip = TR("contacts_settings_tip");
gst.hoverBg = material::WithAlpha(material::OnSurface(), 30);
gst.restBg = material::WithAlpha(material::OnSurface(), 20);
gst.bgRounding = 7.0f * dp;
if (material::IconButton("##contactsSettings", ICON_MD_SETTINGS, material::Type().iconSmall(),
ImVec2(segH, segH), gst))
s_show_contacts_settings = true;
ImGui::SetCursorScreenPos(segOrigin);
ImGui::Dummy(ImVec2(grpW, segH));
}
// Search / filter
ImGui::Spacing();
// Search / filter (tight against the toolbar row above — no extra spacer)
ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x);
ImGui::InputTextWithHint("##ContactSearch", TR("contacts_search_placeholder"),
s_search, sizeof(s_search));
@@ -1009,9 +1056,12 @@ void RenderContactsTab(App* app)
s_selected_index = static_cast<int>(i); s_confirm_delete_idx = -1; openContextMenu = true;
}
bool tAvOnScreen = ImGui::IsRectVisible(tAvP, ImVec2(tAvP.x + tAvR * 2.0f, tAvP.y + tLineH));
// The table is a compact inline view: a full-height "tab" (shape 2) can't render inside a
// table cell, so it falls back to the rounded square there (table scale is fixed too).
const int tShape = (s_contactsShape == 2) ? 1 : s_contactsShape;
drawContactAvatar(ImGui::GetWindowDrawList(), ImVec2(tAvP.x + tAvR, tAvP.y + tLineH * 0.5f),
tAvR, entry, shielded, typeColor(shielded), lightTheme, dp,
material::Type().caption(), material::Type().iconSmall(), tAvOnScreen);
material::Type().caption(), material::Type().iconSmall(), tAvOnScreen, tShape);
ImGui::TableNextColumn();
ImGui::PushFont(material::Type().subtitle2());
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(typeColor(shielded)), "%s", shielded ? "Z" : "T");
@@ -1044,7 +1094,9 @@ void RenderContactsTab(App* app)
});
}
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0,0,0,0));
ImGui::BeginChild("##contactList", ImVec2(0, listH), false, ImGuiWindowFlags_NoBackground);
ImGui::BeginChild("##contactList", ImVec2(0, listH), false,
ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollWithMouse);
material::ApplySmoothScroll(); // wheel-lerp scroll, matching the rest of the app
if (visibleRows.empty()) {
emptyState(ICON_MD_CONTACTS, book.empty() ? "address_book_empty" : "contacts_search_no_match");
} else {
@@ -1056,10 +1108,13 @@ void RenderContactsTab(App* app)
ImFont* lblF = material::Type().subtitle2();
ImFont* adrF = material::Type().caption();
ImFont* icoF = material::Type().iconSmall();
const float sc = s_contactsScale; // card/list scale (avatar + text + row height; not the table)
const float pad = 12.0f * dp;
const float avR = 15.0f * dp;
const float avR = 15.0f * dp * sc;
const float rowH = avR * 2.0f + (asCard ? 16.0f * dp : 12.0f * dp);
const float round = 10.0f * dp;
const float lblSz = lblF->LegacySize * sc; // scaled label + address text
const float adrSz = adrF->LegacySize * sc;
bool rowDeleteRequested = false; // per-row delete is deferred until after the loop
for (size_t vi = 0; vi < visibleRows.size(); ++vi) {
size_t i = visibleRows[vi];
@@ -1103,18 +1158,23 @@ void RenderContactsTab(App* app)
// Only animate when the row is actually on-screen, so a scrolled-off animated avatar
// doesn't hold the app awake (this loop has no clipper).
const bool rowOnScreen = ImGui::IsRectVisible(mn, mx);
drawContactAvatar(dl, avC, avR, entry, shielded, tu, lightTheme, dp, lblF, icoF, rowOnScreen);
// Shape 2 = a full-row-height left tab (a square the height of the row, rounded-left to
// match the row corner); shapes 0/1 are inset badges. Text shifts to clear the tab.
const float tabW = rowH;
drawContactAvatar(dl, avC, avR, entry, shielded, tu, lightTheme, dp, lblF, icoF, rowOnScreen,
s_contactsShape, mn, ImVec2(mn.x + tabW, mx.y), asCard ? round : 0.0f);
// Label (line 1) + muted address (line 2) — reserve trailing room for the globe + actions.
const float actHit = icoF->LegacySize + 8.0f * dp; // per-action square hit area
const float globeW = entry.isGlobal() ? (icoF->LegacySize + 8.0f * dp) : 0.0f;
const float trailW = 3.0f * actHit + 6.0f * dp + globeW;
const float cy = mn.y + rowH * 0.5f;
const float tx = mn.x + pad + avR * 2.0f + pad;
const float tx = (s_contactsShape == 2) ? (mn.x + tabW + pad)
: (mn.x + pad + avR * 2.0f + pad);
const float textMaxX = mx.x - pad - trailW;
const float blockH = lblF->LegacySize + adrF->LegacySize + 3.0f * dp;
const float blockH = lblSz + adrSz + 3.0f * dp;
const float ty = cy - blockH * 0.5f;
dl->PushClipRect(ImVec2(tx, mn.y), ImVec2(textMaxX, mx.y), true);
dl->AddText(lblF, lblF->LegacySize, ImVec2(tx, ty), material::OnSurface(), entry.label.c_str());
dl->AddText(lblF, lblSz, ImVec2(tx, ty), material::OnSurface(), entry.label.c_str());
dl->PopClipRect();
// Un-collapse to the full address on hover (clipped to the text column so it never
// runs under the trailing actions); middle-truncated otherwise.
@@ -1122,7 +1182,7 @@ void RenderContactsTab(App* app)
? entry.address
: util::truncateMiddle(entry.address, addrFrontLbl.truncate, addrBackLbl.truncate);
dl->PushClipRect(ImVec2(tx, mn.y), ImVec2(textMaxX, mx.y), true);
dl->AddText(adrF, adrF->LegacySize, ImVec2(tx, ty + lblF->LegacySize + 3.0f * dp),
dl->AddText(adrF, adrSz, ImVec2(tx, ty + lblSz + 3.0f * dp),
material::OnSurfaceMedium(), addr.c_str());
dl->PopClipRect();
// Trailing: the globe badge stays pinned far-right (global contacts); per-row copy/edit/
@@ -1226,6 +1286,119 @@ void RenderContactsTab(App* app)
ImGui::EndChild();
renderEntryDialog();
// ---- Contacts customization modal (opened by the toolbar gear) — house BlurFloat overlay ----
if (s_show_contacts_settings && app->settings()) {
auto* st = app->settings();
material::OverlayDialogSpec ov;
ov.title = TR("contacts_settings_title");
ov.p_open = &s_show_contacts_settings;
ov.style = material::OverlayStyle::BlurFloat;
ov.cardWidth = 460.0f; ov.idSuffix = "contactsettings";
if (material::BeginOverlayDialog(ov)) {
const float ctrlW = 210.0f * dp;
auto rowStart = [&](const char* label) {
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted(label);
ImGui::SameLine();
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + std::max(0.0f, ImGui::GetContentRegionAvail().x - ctrlW));
ImGui::SetNextItemWidth(ctrlW);
};
// Draw one sample contact card (avatar + label + muted address) at a given shape/scale, using
// the SAME drawContactAvatar + row geometry the real list uses — so the preview matches reality.
auto drawSampleRow = [&](ImDrawList* pdl, ImVec2 mn, float w,
const data::AddressBookEntry& e, int shape, float sc) {
ImFont* lblF = material::Type().subtitle2();
ImFont* adrF = material::Type().caption();
ImFont* icoF = material::Type().iconSmall();
const float pad = 12.0f * dp;
const float avR = 15.0f * dp * sc;
const float rowH = avR * 2.0f + 16.0f * dp;
const float round = 10.0f * dp;
const float lblSz = lblF->LegacySize * sc;
const float adrSz = adrF->LegacySize * sc;
const ImVec2 mx(mn.x + w, mn.y + rowH);
pdl->AddRectFilled(mn, mx, material::WithAlpha(material::OnSurface(), 18), round);
const bool shielded = isShieldedAddr(e.address);
const ImU32 tu = contactTypeColor(shielded, lightTheme);
const ImVec2 avC(mn.x + pad + avR, mn.y + rowH * 0.5f);
const float tabW = rowH; // shape 2 = full-row-height square tab on the left
drawContactAvatar(pdl, avC, avR, e, shielded, tu, lightTheme, dp, lblF, icoF, true,
shape, mn, ImVec2(mn.x + tabW, mx.y), round);
const float tx = (shape == 2) ? (mn.x + tabW + pad) : (mn.x + pad + avR * 2.0f + pad);
const float blockH = lblSz + adrSz + 3.0f * dp;
const float ty = mn.y + rowH * 0.5f - blockH * 0.5f;
const std::string addr = util::truncateMiddle(e.address, addrFrontLbl.truncate, addrBackLbl.truncate);
pdl->PushClipRect(ImVec2(tx, mn.y), ImVec2(mx.x - pad, mx.y), true);
pdl->AddText(lblF, lblSz, ImVec2(tx, ty), material::OnSurface(), e.label.c_str());
pdl->AddText(adrF, adrSz, ImVec2(tx, ty + lblSz + 3.0f * dp), material::OnSurfaceMedium(), addr.c_str());
pdl->PopClipRect();
};
// ---- Live preview: two sample cards at the current shape + scale (updates as controls move). ----
{
const float sc = st->getContactsListScale();
const int shape = st->getContactsAvatarShape();
data::AddressBookEntry sA; sA.label = "Ava Reyes"; sA.address = "zs1preview0shielded0contact0demo0address0q9k";
data::AddressBookEntry sB; sB.label = "Node Ops"; sB.address = "t1PreviewTransparentContactDemoAddr8xQ";
ImDrawList* pdl = ImGui::GetWindowDrawList();
const float padIn = 10.0f * dp, gap = 8.0f * dp;
const float w = ImGui::GetContentRegionAvail().x;
const float avR = 15.0f * dp * sc;
const float rowH = avR * 2.0f + 16.0f * dp;
const float panelH = 2.0f * rowH + gap + 2.0f * padIn;
const ImVec2 pOrigin = ImGui::GetCursorScreenPos();
material::GlassPanelSpec g; g.rounding = 12.0f * dp; g.fillAlpha = 14; g.borderAlpha = 34;
material::DrawGlassPanel(pdl, pOrigin, ImVec2(pOrigin.x + w, pOrigin.y + panelH), g);
const ImVec2 rmn(pOrigin.x + padIn, pOrigin.y + padIn);
const float rowW = w - 2.0f * padIn;
drawSampleRow(pdl, rmn, rowW, sA, shape, sc);
drawSampleRow(pdl, ImVec2(rmn.x, rmn.y + rowH + gap), rowW, sB, shape, sc);
ImGui::Dummy(ImVec2(w, panelH));
ImGui::Dummy(ImVec2(0.0f, 10.0f * dp));
}
// Avatar shape — circle, rounded square, or a full-row-height left tab (segmented control,
// matching the chat settings modal's segmented style; the live preview above reflects it).
{
rowStart(TR("contacts_avatar_shape"));
const char* items[] = { TR("contacts_shape_circle"), TR("contacts_shape_square"),
TR("contacts_shape_tab") };
const float segH = ImGui::GetFrameHeight();
const ImVec2 segOrigin = ImGui::GetCursorScreenPos();
const int cur = st->getContactsAvatarShape();
const int clk = material::SegmentedControl(ImGui::GetWindowDrawList(), segOrigin, ctrlW, segH,
items, 3, cur, material::Type().caption(),
"##contactsShape", dp);
ImGui::SetCursorScreenPos(segOrigin);
ImGui::Dummy(ImVec2(ctrlW, segH)); // restore + reserve the control's rect for layout flow
if (clk >= 0 && clk != cur) { st->setContactsAvatarShape(clk); st->save(); }
}
// Card/list row scale (does not affect the table view).
{
ImGui::Dummy(ImVec2(0.0f, 4.0f * dp));
rowStart(TR("contacts_list_scale"));
float v = st->getContactsListScale();
if (ImGui::SliderFloat("##contactsScale", &v, 0.8f, 1.5f, "%.2fx", ImGuiSliderFlags_AlwaysClamp)) {
st->setContactsListScale(v); st->save();
}
}
ImGui::Dummy(ImVec2(0.0f, 8.0f * dp));
{
// Done: sized to its text (+ a little shoulder room) and centered, not full width.
const char* doneLbl = TR("chat_settings_done");
ImGui::PushFont(material::Type().button());
const float bw = ImGui::CalcTextSize(doneLbl).x + ImGui::GetStyle().FramePadding.x * 2.0f + 28.0f * dp;
ImGui::PopFont();
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + std::max(0.0f, (ImGui::GetContentRegionAvail().x - bw) * 0.5f));
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(material::WithAlpha(material::Primary(), 205)));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(material::Primary()));
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImGui::ColorConvertU32ToFloat4(material::WithAlpha(material::Primary(), 235)));
if (material::TactileButton(doneLbl, ImVec2(bw, 0)))
s_show_contacts_settings = false;
ImGui::PopStyleColor(3);
}
material::EndOverlayDialog();
}
}
}
void ContactsSweepOpenEditDialog(int avatarMode)

View File

@@ -47,6 +47,7 @@ struct MarketViewState {
int chartStyle = 1; // 0 = line, 1 = candlestick (only applies when per-exchange OHLC exists)
};
static MarketViewState s_mkt;
static bool s_show_market_settings = false; // Market general-options modal (gear button, like chat)
// The effective exchange registry: the live CoinGecko list when available, else the
// compiled-in fallback.
@@ -1080,6 +1081,21 @@ bool PortfolioEditorActive() { return s_pfEdit.open; }
// 2=value-hero. No left accent strip — identity comes from the tinted icon + a subtle accent
// border. Fixed right-aligned columns keep numbers aligned across rows; on-but-absent fields show a
// muted em-dash so a row never looks broken. Pure drawing into dl.
// TABLE-style (portfolio_style 0) right-edge column anchors — shared by pfDrawRow's row body and the
// column-header strip so the header labels sit exactly over the values they name.
struct PfTableCols { float labelR, drgxR, valR, chgR; };
static PfTableCols pfTableCols(float right, float dp)
{
const float colGap = Layout::spacingMd();
const float chgW = 62.0f*dp, valW = 108.0f*dp, drgxW = 118.0f*dp;
PfTableCols c;
c.chgR = right;
c.valR = c.chgR - chgW - colGap;
c.drgxR = c.valR - valW - colGap;
c.labelR = c.drgxR - drgxW - colGap;
return c;
}
static void pfDrawRow(ImDrawList* dl, ImVec2 rowMin, ImVec2 rowMax,
const config::Settings::PortfolioEntry& e, const WalletState& state,
const MarketInfo& market, int style, float dp,
@@ -1087,17 +1103,39 @@ static void pfDrawRow(ImDrawList* dl, ImVec2 rowMin, ImVec2 rowMax,
{
double bal = data::SumPortfolioBalance(e.addresses, state.addresses);
ImU32 accent = e.color ? (ImU32)e.color : 0;
const bool zeroBal = (bal <= 0.0); // empty groups render dimmed (content only, not the container)
// Card — a touch more fill than before so rows read as distinct cards, plus a subtle accent
// border for identity (the left accent strip was removed).
// ---- Per-style CONTAINER (the enabler): the three styles differ before any field is read.
// Table (0) = borderless ledger row (hairline + hover wash + a slim left identity tick);
// Cards (1) = glass card with a faint accent tint + a left accent spine;
// Spotlight (2) = a heavier tile (higher fill + a bold accent spine). ----
const float round = 10.0f * dp;
const int oa = (int)(std::max(0, std::min(100, e.outlineOpacity)) * 2.55f + 0.5f);
if (style == 0) {
if (hov) dl->AddRectFilled(rowMin, rowMax, WithAlpha(OnSurface(), 14), 0.0f);
dl->AddLine(ImVec2(rowMin.x, rowMax.y - 0.5f), ImVec2(rowMax.x, rowMax.y - 0.5f),
WithAlpha(OnSurface(), 24), 1.0f);
dl->AddRectFilled(ImVec2(rowMin.x, rowMin.y + 5.0f * dp), ImVec2(rowMin.x + 3.0f * dp, rowMax.y - 5.0f * dp),
accent ? accent : WithAlpha(OnSurface(), 55), 1.5f * dp);
} else if (style == 1) {
GlassPanelSpec g; g.rounding = 10.0f; g.fillAlpha = hov ? 42 : 30; g.borderAlpha = 55;
DrawGlassPanel(dl, rowMin, rowMax, g);
if (accent) {
int oa = (int)(std::max(0, std::min(100, e.outlineOpacity)) * 2.55f + 0.5f);
dl->AddRectFilled(rowMin, rowMax, WithAlpha(accent, hov ? 16 : 10), round);
dl->AddRectFilled(rowMin, ImVec2(rowMin.x + 3.0f * dp, rowMax.y), WithAlpha(accent, 205),
round, ImDrawFlags_RoundCornersLeft);
dl->AddRect(rowMin, rowMax, WithAlpha(accent, hov ? std::min(255, oa + 45) : oa), 10.0f, 0, hov ? 1.8f : 1.2f);
} else if (hov) {
dl->AddRect(rowMin, rowMax, WithAlpha(OnSurface(), 80), 10.0f, 0, 1.2f);
}
} else {
GlassPanelSpec g; g.rounding = 10.0f; g.fillAlpha = hov ? 56 : 46; g.borderAlpha = 60;
DrawGlassPanel(dl, rowMin, rowMax, g);
dl->AddRectFilled(rowMin, ImVec2(rowMin.x + 4.0f * dp, rowMax.y),
WithAlpha(accent ? accent : Primary(), 220), round, ImDrawFlags_RoundCornersLeft);
if (accent) dl->AddRect(rowMin, rowMax, WithAlpha(accent, hov ? std::min(255, oa + 45) : oa), 10.0f, 0, hov ? 1.8f : 1.2f);
else if (hov) dl->AddRect(rowMin, rowMax, WithAlpha(OnSurface(), 70), 10.0f, 0, 1.2f);
}
auto tw = [](ImFont* f, const std::string& s){ return f->CalcTextSizeA(f->LegacySize, FLT_MAX, 0, s.c_str()).x; };
auto fit = [&](std::string s, ImFont* f, float maxW){ bool t=false; while (s.size()>1 && tw(f,s)>maxW){ s.pop_back(); t=true; } if (t) s += "\xE2\x80\xA6"; return s; };
@@ -1130,76 +1168,116 @@ static void pfDrawRow(ImDrawList* dl, ImVec2 rowMin, ImVec2 rowMax,
float right = rowMax.x - padX;
float midY = (rowMin.y + rowMax.y) * 0.5f;
// Empty groups: dim the BODY content (text/pill/bar/sparkline) to ~half alpha so a $0 group reads as
// muted, while the container (card/tick) stays full-strength. Done by scaling the alpha of the
// vertices this body emits — one place instead of tinting every draw call.
const int zdVtx = zeroBal ? dl->VtxBuffer.Size : -1;
if (style == 0) {
// ---- Compact: icon + label (left); fixed right-aligned columns DRGX | value | 24h | spark.
float iconSz = sub1->LegacySize, x = left;
// ---- TABLE: icon + label (left); tight numeric columns DRGX | value | 24h pill (right).
// No sparkline — the trend rides in a small coloured % pill, keeping this a scannable grid.
const PfTableCols cols = pfTableCols(right, dp);
const float chgR = cols.chgR, valR = cols.valR, drgxR = cols.drgxR, labelR = cols.labelR;
const float valW = 108.0f*dp, drgxW = 118.0f*dp; // fit() widths for the value / DRGX columns
float iconSz = capFont->LegacySize, x = left;
if (!e.icon.empty()) {
material::project_icons::drawByName(dl, e.icon.c_str(), ImVec2(x+iconSz*0.5f, midY), accent?accent:OnSurfaceMedium(), Type().iconSmall(), iconSz);
x += iconSz + Layout::spacingSm();
}
const float sparkW = 92.0f*dp, chgW = 60.0f*dp, valW = 112.0f*dp, drgxW = 122.0f*dp;
float sparkL = right - sparkW;
float chgR = sparkL - colGap;
float valR = chgR - chgW - colGap;
float drgxR = valR - valW - colGap;
float labelR = drgxR - drgxW - colGap;
if (e.showDrgx) rtext(capFont, drgxR, midY, OnSurfaceMedium(), fit(drgxStr, capFont, drgxW));
if (wantValue) rtext(sub1, valR, midY, hasValue?OnSurface():OnSurfaceDisabled(), hasValue?fit(valStr,sub1,valW):kDash);
if (wantChange) rtext(capFont, chgR, midY, hasChange?chgCol:OnSurfaceDisabled(), hasChange?chgStr:kDash);
if (hasSpark) pfDrawSparkline(dl, ImVec2(sparkL, rowMin.y+padY), ImVec2(right, rowMax.y-padY), spark, sparkCol);
float lblMaxW = std::max(24.0f*dp, labelR - x);
if (wantChange) {
const std::string s = hasChange ? chgStr : kDash;
const float pw = tw(capFont, s) + 12.0f*dp, ph = capFont->LegacySize + 5.0f*dp;
const ImVec2 pmn(chgR - pw, midY - ph*0.5f);
if (hasChange) dl->AddRectFilled(pmn, ImVec2(chgR, midY + ph*0.5f), WithAlpha(chgCol, 32), ph*0.5f);
dl->AddText(capFont, capFont->LegacySize, ImVec2(pmn.x + 6.0f*dp, midY - capFont->LegacySize*0.5f),
hasChange?chgCol:OnSurfaceDisabled(), s.c_str());
}
const float lblMaxW = std::max(24.0f*dp, labelR - x);
dl->AddText(sub1, sub1->LegacySize, ImVec2(x, midY - sub1->LegacySize*0.5f), OnSurface(), fit(e.label,sub1,lblMaxW).c_str());
} else if (style == 1) {
// ---- Detailed: 2x2 grid — label/value on top, DRGX/24h below — plus a tall sparkline strip.
float iconSz = sub1->LegacySize, x = left;
const float sparkW = 150.0f*dp;
float sparkL = right - sparkW;
float textR = hasSpark ? (sparkL - colGap) : right;
float topY = rowMin.y + padY;
float botY = rowMax.y - padY - capFont->LegacySize;
if (!e.icon.empty()) {
material::project_icons::drawByName(dl, e.icon.c_str(), ImVec2(x+iconSz*0.5f, topY+iconSz*0.5f), accent?accent:OnSurfaceMedium(), Type().iconSmall(), iconSz);
x += iconSz + Layout::spacingSm();
}
// value (top-right, emphasized)
std::string vTop = wantValue ? (hasValue?valStr:kDash) : std::string();
if (wantValue) dl->AddText(sub1, sub1->LegacySize, ImVec2(textR - tw(sub1,vTop), topY), hasValue?OnSurface():OnSurfaceDisabled(), vTop.c_str());
// label (top-left, fills up to the value column)
float labelR = wantValue ? (textR - tw(sub1,vTop) - colGap) : textR;
dl->AddText(sub1, sub1->LegacySize, ImVec2(x, topY), OnSurface(), fit(e.label,sub1,std::max(24.0f*dp, labelR - x)).c_str());
// DRGX (bottom-left, muted) + 24h (bottom-right, colored)
if (e.showDrgx) dl->AddText(capFont, capFont->LegacySize, ImVec2(left, botY), OnSurfaceMedium(), drgxStr.c_str());
if (wantChange) { std::string s = hasChange?chgStr:kDash; dl->AddText(capFont, capFont->LegacySize, ImVec2(textR - tw(capFont,s), botY), hasChange?chgCol:OnSurfaceDisabled(), s.c_str()); }
// tall sparkline strip spanning both lines
if (hasSpark) pfDrawSparklineFilled(dl, ImVec2(sparkL, topY), ImVec2(right, rowMax.y-padY), spark, sparkCol);
} else {
// ---- Value-hero: label; big neutral value; 24h; DRGX + sparkline fill the right.
// ---- CARDS: muted label + a leading (shadowed) value on top; DRGX + 24h beneath; and a
// per-group shielded/transparent split bar along the bottom edge as the signature.
const float zbarH = 3.0f*dp;
float iconSz = capFont->LegacySize, x = left;
const float topY = rowMin.y + padY + 1.0f*dp;
const float botY = rowMax.y - padY - zbarH - 3.0f*dp - capFont->LegacySize;
if (!e.icon.empty()) {
material::project_icons::drawByName(dl, e.icon.c_str(), ImVec2(x+iconSz*0.5f, topY+sub1->LegacySize*0.5f), accent?accent:OnSurfaceMedium(), Type().iconSmall(), iconSz);
x += iconSz + Layout::spacingSm();
}
// value (top-right, leads via a soft shadow)
std::string vTop = wantValue ? (hasValue?valStr:kDash) : std::string();
float vW = wantValue ? tw(sub1, vTop) : 0.0f;
if (wantValue) DrawTextShadow(dl, sub1, sub1->LegacySize, ImVec2(right - vW, topY), hasValue?OnSurface():OnSurfaceDisabled(), vTop.c_str());
// label (top-left, muted so the value leads)
float labelR = wantValue ? (right - vW - colGap) : right;
dl->AddText(capFont, capFont->LegacySize, ImVec2(x, topY + (sub1->LegacySize - capFont->LegacySize)*0.5f), OnSurfaceMedium(), fit(e.label,capFont,std::max(24.0f*dp, labelR - x)).c_str());
// DRGX (bottom-left) + 24h (bottom-right)
if (e.showDrgx) dl->AddText(capFont, capFont->LegacySize, ImVec2(left, botY), OnSurfaceMedium(), drgxStr.c_str());
if (wantChange) { std::string s = hasChange?chgStr:kDash; dl->AddText(capFont, capFont->LegacySize, ImVec2(right - tw(capFont,s), botY), hasChange?chgCol:OnSurfaceDisabled(), s.c_str()); }
// shielded/transparent split bar (Cards-only signature; read-only balance data)
{
const data::PortfolioSplit sp = data::SumPortfolioSplit(e.addresses, state.addresses);
const double tot = sp.shielded + sp.transparent;
const float barY = rowMax.y - padY - zbarH;
const bool barLight = IsLightTheme();
dl->AddRectFilled(ImVec2(left, barY), ImVec2(right, barY + zbarH),
barLight ? IM_COL32(0,0,0,20) : IM_COL32(255,255,255,12), zbarH*0.5f);
if (tot > 0) {
float sr = (float)(sp.shielded / tot); sr = sr<0?0.0f:(sr>1?1.0f:sr);
float sw = (right - left) * sr;
const int fa = barLight ? 205 : 175;
if (sw > 0.5f) dl->AddRectFilled(ImVec2(left, barY), ImVec2(left+sw, barY+zbarH), WithAlpha(Success(), fa),
(right-left-sw > 0.5f) ? ImDrawFlags_RoundCornersLeft : ImDrawFlags_RoundCornersAll, zbarH*0.5f);
if (right-left-sw > 0.5f) dl->AddRectFilled(ImVec2(left+sw, barY), ImVec2(right, barY+zbarH), WithAlpha(Warning(), fa),
sw > 0.5f ? ImDrawFlags_RoundCornersRight : ImDrawFlags_RoundCornersAll, zbarH*0.5f);
}
}
} else {
// ---- SPOTLIGHT: muted name + DRGX on top, then a LARGE value coloured by 24h direction with a
// delta chip, and a filled sparkline on the right. A third font tier no other style uses.
const float heroSz = sub1->LegacySize * 1.5f; // ImGui 1.92 rebakes at this size (crisp, not upscaled)
const float sparkW = (right - left) * 0.34f;
const float sparkL = right - sparkW;
const float rightZoneL = hasSpark ? (sparkL - colGap) : right;
const float blockH = capFont->LegacySize + Layout::spacingXs() + heroSz;
const float topY = midY - blockH * 0.5f;
float iconSz = capFont->LegacySize, x = left;
const float sparkW = (right-left)*0.40f;
float sparkL = right - sparkW;
float topY = rowMin.y + padY;
if (!e.icon.empty()) {
material::project_icons::drawByName(dl, e.icon.c_str(), ImVec2(x+iconSz*0.5f, topY+iconSz*0.5f), accent?accent:OnSurfaceMedium(), Type().iconSmall(), iconSz);
x += iconSz + Layout::spacingSm();
}
float rightZoneL = hasSpark ? sparkL : right;
dl->AddText(capFont, capFont->LegacySize, ImVec2(x, topY), OnSurfaceMedium(), fit(e.label,capFont,std::max(24.0f*dp, rightZoneL - x - colGap)).c_str());
// hero value — neutral bold (accent stays in the icon/border, so it can't clash with the 24h)
float valY = topY + capFont->LegacySize + Layout::spacingXs();
std::string hero = hasValue ? valStr : (e.showDrgx ? drgxStr : kDash);
DrawTextShadow(dl, sub1, sub1->LegacySize, ImVec2(left, valY), hasValue?OnSurface():OnSurfaceDisabled(), hero.c_str());
// 24h below the value (colored / dash)
float botY = valY + sub1->LegacySize + Layout::spacingXs();
if (wantChange) dl->AddText(capFont, capFont->LegacySize, ImVec2(left, botY), hasChange?chgCol:OnSurfaceDisabled(), (hasChange?chgStr:kDash).c_str());
// DRGX — right-aligned on the value line; fills the right when there's no sparkline.
if (e.showDrgx && hasValue) {
float dr = hasSpark ? (sparkL - colGap) : right;
dl->AddText(capFont, capFont->LegacySize, ImVec2(dr - tw(capFont,drgxStr), valY + (sub1->LegacySize - capFont->LegacySize)*0.5f), OnSurfaceMedium(), drgxStr.c_str());
// name (top-left, muted) + DRGX (top-right, muted)
const float drgxW2 = e.showDrgx ? tw(capFont, drgxStr) : 0.0f;
if (e.showDrgx) rtext(capFont, rightZoneL, topY + capFont->LegacySize*0.5f, OnSurfaceMedium(), drgxStr);
const float nameMaxW = std::max(24.0f*dp, (rightZoneL - (e.showDrgx ? drgxW2 + colGap : 0.0f)) - x);
dl->AddText(capFont, capFont->LegacySize, ImVec2(x, topY), OnSurfaceMedium(), fit(e.label,capFont,nameMaxW).c_str());
// hero value — coloured by 24h direction (neutral when no live change)
const float valY = topY + capFont->LegacySize + Layout::spacingXs();
const std::string hero = hasValue ? valStr : (e.showDrgx ? drgxStr : kDash);
const ImU32 heroCol = !hasValue ? OnSurfaceDisabled() : (hasChange ? chgCol : OnSurface());
DrawTextShadow(dl, sub1, heroSz, ImVec2(left, valY), heroCol, hero.c_str());
const float heroW = sub1->CalcTextSizeA(heroSz, FLT_MAX, 0, hero.c_str()).x;
// delta chip trailing the hero value (only if it fits before the sparkline zone)
if (wantChange && hasChange) {
const float pw = tw(capFont, chgStr) + 12.0f*dp, ph = capFont->LegacySize + 5.0f*dp;
const float chipX = left + heroW + Layout::spacingSm();
const float chipY = valY + (heroSz - ph)*0.5f;
if (chipX + pw <= rightZoneL) {
dl->AddRectFilled(ImVec2(chipX, chipY), ImVec2(chipX+pw, chipY+ph), WithAlpha(chgCol, 34), ph*0.5f);
dl->AddText(capFont, capFont->LegacySize, ImVec2(chipX+6.0f*dp, chipY + (ph-capFont->LegacySize)*0.5f), chgCol, chgStr.c_str());
}
}
if (hasSpark) pfDrawSparklineFilled(dl, ImVec2(sparkL, rowMin.y + padY), ImVec2(right, rowMax.y - padY), spark, sparkCol);
}
if (zdVtx >= 0) { // fade the just-emitted body vertices for an empty group
for (int vi = zdVtx; vi < dl->VtxBuffer.Size; ++vi) {
ImDrawVert& v = dl->VtxBuffer[vi];
v.col = material::ScaleAlpha(v.col, 0.5f);
}
// filled sparkline (right ~40%)
if (hasSpark) pfDrawSparklineFilled(dl, ImVec2(sparkL, valY - 2*dp), ImVec2(right, rowMax.y-padY), spark, sparkCol);
}
}
@@ -1867,12 +1945,23 @@ static void mktDrawPortfolio(const MktCtx& cx)
}
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("market_portfolio"));
// "Manage…" button, right-aligned on the header row — opens the portfolio editor.
// Settings gear + "Manage…" button, right-aligned on the header row (gear left of Manage). The gear
// opens the general market-options modal; Manage opens the portfolio editor.
{
const char* ml = TR("portfolio_manage");
float mBtnW = body2->CalcTextSizeA(body2->LegacySize, FLT_MAX, 0, ml).x + Layout::spacingMd() * 2;
const float gearW = ImGui::GetFrameHeight(); // square, matching the Manage button's height
const float gGap = Layout::spacingXs();
ImGui::SameLine();
material::RightAlignX(mBtnW);
RightAlignX(mBtnW + gGap + gearW); // reserve the whole group before drawing left-to-right
IconButtonStyle gst;
gst.tooltip = TR("market_settings_tip");
gst.hoverBg = WithAlpha(OnSurface(), 30);
gst.restBg = WithAlpha(OnSurface(), 20);
gst.bgRounding = 7.0f * mktDp;
if (IconButton("##marketSettings", ICON_MD_SETTINGS, Type().iconSmall(), ImVec2(gearW, gearW), gst))
s_show_market_settings = true;
ImGui::SameLine(0.0f, gGap);
if (material::TactileButton(ml, ImVec2(mBtnW, 0))) {
// Open the editor on the first group visible to this wallet (or the empty state if none) —
// never a raw index 0 that might belong to a different wallet.
@@ -1982,8 +2071,9 @@ static void mktDrawPortfolio(const MktCtx& cx)
}
int style = app->settings() ? app->settings()->getPortfolioStyle() : 0;
float rowH = (style == 0 ? 46.0f : style == 1 ? 64.0f : 84.0f) * mktDp;
float rowGap = Layout::spacingSm();
float rowH = (style == 0 ? 40.0f : style == 1 ? 68.0f : 80.0f) * mktDp;
// Table rows abut like a ledger (thin gap); Cards/Spotlight breathe.
float rowGap = (style == 0 ? 2.0f * mktDp : Layout::spacingSm());
float rowsH = std::max(rowH, portfolioH - pfSummaryH);
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMin.y + pfSummaryH));
@@ -2002,6 +2092,29 @@ static void mktDrawPortfolio(const MktCtx& cx)
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("portfolio_no_entries"));
} else {
// TABLE column-header strip (style 0): a thin static header labelling the numeric columns —
// a signature the card styles don't have. Uses the shared pfTableCols so it aligns with rows.
if (style == 0) {
const float headerH = capFont->LegacySize + 8.0f * mktDp;
const ImVec2 hp = ImGui::GetCursorScreenPos();
const float hLeft = hp.x + Layout::spacingMd();
const float hRight = hp.x + rowW - Layout::spacingMd();
const PfTableCols hc = pfTableCols(hRight, mktDp);
ImFont* ovF = Type().overline();
const float ty = hp.y + (headerH - ovF->LegacySize) * 0.5f;
const ImU32 hcol = OnSurfaceDisabled();
auto hdr = [&](float xr, const char* s){
const float w = ovF->CalcTextSizeA(ovF->LegacySize, FLT_MAX, 0, s).x;
rdl->AddText(ovF, ovF->LegacySize, ImVec2(xr - w, ty), hcol, s);
};
rdl->AddText(ovF, ovF->LegacySize, ImVec2(hLeft, ty), hcol, TR("market_col_name"));
hdr(hc.drgxR, DRAGONX_TICKER);
hdr(hc.valR, TR("market_col_value"));
hdr(hc.chgR, TR("market_24h"));
rdl->AddLine(ImVec2(hLeft, hp.y + headerH - 1.0f), ImVec2(hRight, hp.y + headerH - 1.0f),
WithAlpha(OnSurface(), 20), 1.0f);
ImGui::Dummy(ImVec2(rowW, headerH));
}
for (int vi = 0; vi < (int)vis.size(); vi++) {
int i = vis[vi];
ImVec2 rMin = ImGui::GetCursorScreenPos();
@@ -2025,6 +2138,87 @@ static void mktDrawPortfolio(const MktCtx& cx)
ImGui::Dummy(ImVec2(0, gap));
}
// General market-tab options modal (opened by the PORTFOLIO-header gear). Mirrors the chat/contacts
// settings modals: a BlurFloat overlay of right-aligned segmented controls + a text-sized Done button.
// All controls write EXISTING settings (+ sync the live s_mkt mirror for chart range/style so the chart
// updates the same frame). Rendered outside the scroll child so it overlays the page.
static void RenderMarketSettings(App* app)
{
if (!s_show_market_settings || !app->settings()) return;
auto* st = app->settings();
const float dp = Layout::dpiScale();
OverlayDialogSpec ov;
ov.title = TR("market_settings_title");
ov.p_open = &s_show_market_settings;
ov.style = OverlayStyle::BlurFloat;
ov.cardWidth = 480.0f;
ov.idSuffix = "marketsettings";
if (BeginOverlayDialog(ov)) {
const float ctrlW = 280.0f * dp; // roomy enough for the multi-word segment labels
auto rowStart = [&](const char* label) {
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted(label);
ImGui::SameLine();
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + std::max(0.0f, ImGui::GetContentRegionAvail().x - ctrlW));
};
// A right-aligned segmented control on its own row; returns the clicked index (-1 if none).
auto seg = [&](const char* label, const char* const* items, int count, int cur, const char* id) -> int {
rowStart(label);
const float segH = ImGui::GetFrameHeight();
const ImVec2 o = ImGui::GetCursorScreenPos();
int clk = SegmentedControl(ImGui::GetWindowDrawList(), o, ctrlW, segH,
items, count, cur, Type().caption(), id, dp);
ImGui::SetCursorScreenPos(o);
ImGui::Dummy(ImVec2(ctrlW, segH)); // restore + reserve the control's rect for layout flow
return clk;
};
// Portfolio layout — surfaces the row style that was previously only Left/Right-arrow cyclable,
// and (unlike the arrow cycle) persists it.
{
const char* items[] = { TR("portfolio_style_compact"), TR("portfolio_style_detailed"),
TR("portfolio_style_featured") };
const int cur = st->getPortfolioStyle();
const int clk = seg(TR("portfolio_style_label"), items, 3, cur, "##mktPfStyle");
if (clk >= 0 && clk != cur) { st->setPortfolioStyle(clk); st->save(); }
}
// (Chart RANGE is deliberately not duplicated here — the Live/1H/1D/1W/1M toggle is always
// visible on the chart itself, and a 5-segment control can't hold long "Live" translations.)
// Chart style (candlesticks only render where per-exchange OHLC exists; the setting is still stored).
{
ImGui::Dummy(ImVec2(0.0f, 4.0f * dp));
const char* items[] = { TR("market_style_line_label"), TR("market_style_candle_label") };
const int cur = st->getChartStyle();
const int clk = seg(TR("market_opt_chart_style"), items, 2, cur, "##mktChartStyle");
if (clk >= 0 && clk != cur) { s_mkt.chartStyle = clk; st->setChartStyle(clk); st->save(); }
}
// Fetch live prices (master toggle, mirrors the general Settings page).
{
ImGui::Dummy(ImVec2(0.0f, 6.0f * dp));
bool v = st->getFetchPrices();
if (ImGui::Checkbox(TR("fetch_prices"), &v)) { st->setFetchPrices(v); st->save(); }
}
// Done — sized to its text (+ a little shoulder room) and centered, not full width.
ImGui::Dummy(ImVec2(0.0f, 8.0f * dp));
{
const char* doneLbl = TR("chat_settings_done");
ImGui::PushFont(Type().button());
const float bw = ImGui::CalcTextSize(doneLbl).x + ImGui::GetStyle().FramePadding.x * 2.0f + 28.0f * dp;
ImGui::PopFont();
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + std::max(0.0f, (ImGui::GetContentRegionAvail().x - bw) * 0.5f));
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(WithAlpha(Primary(), 205)));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(Primary()));
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImGui::ColorConvertU32ToFloat4(WithAlpha(Primary(), 235)));
if (material::TactileButton(doneLbl, ImVec2(bw, 0)))
s_show_market_settings = false;
ImGui::PopStyleColor(3);
}
EndOverlayDialog();
}
}
void RenderMarketTab(App* app)
{
auto& S = schema::UI();
@@ -2058,14 +2252,16 @@ void RenderMarketTab(App* app)
// Left/Right arrows: cycle portfolio row styles (compact / detailed / featured), mirroring the
// Overview tab's layout switch. Skip while typing, when Ctrl is held (theme cycle), or while the
// portfolio editor modal is open.
if (app->settings() && !s_pfEdit.open && !ImGui::GetIO().WantTextInput && !ImGui::GetIO().KeyCtrl) {
// portfolio editor / market-settings modal is open.
if (app->settings() && !s_pfEdit.open && !s_show_market_settings &&
!ImGui::GetIO().WantTextInput && !ImGui::GetIO().KeyCtrl) {
bool prev = ImGui::IsKeyPressed(ImGuiKey_LeftArrow);
bool next = ImGui::IsKeyPressed(ImGuiKey_RightArrow);
if (prev || next) {
int st = app->settings()->getPortfolioStyle();
st = next ? (st + 1) % 3 : (st + 2) % 3;
app->settings()->setPortfolioStyle(st);
app->settings()->save(); // persist the choice (matches the gear-modal control)
const char* names[3] = { TR("portfolio_style_compact"), TR("portfolio_style_detailed"),
TR("portfolio_style_featured") };
Notifications::instance().info(std::string(TR("portfolio_style_label")) + ": " + names[st]);
@@ -2123,15 +2319,16 @@ void RenderMarketTab(App* app)
for (const auto& e : pfEntriesGeo)
if (e.scope.empty() || (!pfActiveHash.empty() && e.scope == pfActiveHash)) pfVisN++;
int pfStyle = app->settings()->getPortfolioStyle();
float pfRowH = (pfStyle == 0 ? 46.0f : pfStyle == 1 ? 64.0f : 84.0f) * mktDp;
float pfRowGap = Layout::spacingSm();
float pfRowH = (pfStyle == 0 ? 40.0f : pfStyle == 1 ? 68.0f : 80.0f) * mktDp;
float pfRowGap = (pfStyle == 0 ? 2.0f * mktDp : Layout::spacingSm()); // Table rows abut
float pfHeaderH = (pfStyle == 0) ? (capFont->LegacySize + 8.0f * mktDp) : 0.0f; // Table column strip
const int pfMaxVisibleRows = 4; // rows shown before the list scrolls
// Height for up to pfMaxVisibleRows rows. ItemSpacing is zeroed inside the row child, so the
// content is exactly rows + inter-row gaps; one extra gap of bottom breathing room keeps the
// last visible row off the clip edge. More groups than this scroll internally.
int pfVisRows = std::min(pfVisN, pfMaxVisibleRows);
float pfGroupsH = (pfVisRows > 0)
? (pfVisRows * pfRowH + (pfVisRows - 1) * pfRowGap + pfRowGap)
? (pfHeaderH + pfVisRows * pfRowH + (pfVisRows - 1) * pfRowGap + pfRowGap)
: 0.0f;
float portfolioH = pfSummaryH + pfGroupsH;
@@ -2192,6 +2389,7 @@ void RenderMarketTab(App* app)
// Portfolio editor modal (rendered outside the scroll child so it overlays the page). It captures
// + blurs the live content drawn above, so it must render after the tab body.
RenderPortfolioEditor(app);
RenderMarketSettings(app);
}
} // namespace ui

View File

@@ -98,7 +98,12 @@ void RenderPeersTab(App* app)
// Scrollable child to contain all content within available space
ImVec2 peersAvail = ImGui::GetContentRegionAvail();
ImGui::BeginChild("##PeersScroll", peersAvail, false, ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollbar);
// NoScrollWithMouse: the inner ##PeersList owns wheel scrolling (via ApplySmoothScroll). Without
// this, a wheel over the list would scroll BOTH the list (smooth-scroll) and this outer container
// (ImGui forwards the NoScrollWithMouse child's wheel to its scrollable ancestor) — a double-scroll.
// Safe because the peer panel is sized to fill the remaining height, so this outer never overflows.
ImGui::BeginChild("##PeersScroll", peersAvail, false,
ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
// Responsive: scale factors per frame
float availWidth = ImGui::GetContentRegionAvail().x;

View File

@@ -432,6 +432,13 @@ void I18n::loadBuiltinEnglish()
strings_["mig_done_detail"] = "The daemon is rescanning to show your funds — this can take a few minutes. Your previous wallet was saved as a .bak in the data folder.";
strings_["mig_done_btn"] = "Done";
strings_["contacts_search_placeholder"] = "Search contacts...";
strings_["contacts_settings_title"] = "Contacts settings";
strings_["contacts_settings_tip"] = "Contacts customization";
strings_["contacts_avatar_shape"] = "Avatar shape";
strings_["contacts_shape_circle"] = "Circle";
strings_["contacts_shape_square"] = "Square";
strings_["contacts_shape_tab"] = "Left tab";
strings_["contacts_list_scale"] = "List scale";
strings_["contacts_search_no_match"] = "No matching contacts";
strings_["address_book_confirm_delete"] = "Confirm delete?";
strings_["mining"] = "Mining";
@@ -1559,6 +1566,8 @@ void I18n::loadBuiltinEnglish()
strings_["market_12h"] = "12h";
strings_["market_18h"] = "18h";
strings_["market_24h"] = "24h";
strings_["market_col_name"] = "Name";
strings_["market_col_value"] = "Value";
strings_["market_24h_volume"] = "24H VOLUME";
strings_["market_6h"] = "6h";
strings_["market_iv_live"] = "Live";
@@ -1575,6 +1584,11 @@ void I18n::loadBuiltinEnglish()
strings_["market_chart_loading"] = "Loading price history";
strings_["market_style_line"] = "Switch to line chart";
strings_["market_style_candle"] = "Switch to candlesticks";
strings_["market_style_line_label"] = "Line";
strings_["market_style_candle_label"] = "Candlestick";
strings_["market_settings_title"] = "Market settings";
strings_["market_settings_tip"] = "Market options";
strings_["market_opt_chart_style"] = "Chart style";
strings_["market_no_price"] = "No price data";
strings_["market_now"] = "Now";
strings_["market_pct_shielded"] = "%.0f%% Shielded";
@@ -1622,9 +1636,9 @@ void I18n::loadBuiltinEnglish()
strings_["portfolio_clear_sel"] = "Clear";
strings_["portfolio_no_entries"] = "No custom entries yet. Add one to track a group of addresses.";
strings_["portfolio_style_label"] = "Portfolio style";
strings_["portfolio_style_compact"] = "Compact rows";
strings_["portfolio_style_detailed"] = "Detailed rows";
strings_["portfolio_style_featured"] = "Featured rows";
strings_["portfolio_style_compact"] = "Table";
strings_["portfolio_style_detailed"] = "Cards";
strings_["portfolio_style_featured"] = "Spotlight";
strings_["portfolio_edit"] = "Edit";
strings_["portfolio_delete"] = "Delete";
strings_["portfolio_save"] = "Save";