T16 Finance: market charts with kinds, volume, profile, studies, drawings, compare, alerts and sync (T16a–b); quotes, book, tape and DOM (T16c); order entry, blotter, risk, options and payoff (T16d); markets, calendars, portfolio and wallet (T16e); typography::Ellipsis ends lines that gpui's truncate clipped; chart notes stack and ticks label round values

This commit is contained in:
ZacharyZhang-NY committed 2026-09-25 23:30:04 -04:00
1 parent efde9d64b0
commit 8d43b7a880
76 files changed
+10311 -173

No files matched your search

+1 -12
View File
@@ -12,24 +12,13 @@ use gpui::{App, ClipboardItem, IntoElement, ParentElement, SharedString, Styled,
use crate::{
probe::probe,
ui::{keep, row, section, set},
ui::{keep, noise, row, section, set},
};
const MONTHS: [&str; 12] = [
"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
];
/// A steady pseudo-random series, so captures repeat.
pub(super) fn noise(seed: u64) -> impl FnMut() -> f64 {
let mut state = seed;
move || {
state = state
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
(state >> 33) as f64 / (1u64 << 31) as f64
}
}
pub fn lines(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let turn = keep("lines-turn", || 0usize, window, cx);
let (now, next) = (*turn.read(cx), turn.clone());
+4 -2
View File
@@ -5,8 +5,10 @@ use ely_gpui_component::charts::{
use gpui::{App, IntoElement, ParentElement, Styled, div, px};
use jiff::civil::date;
use super::cartesian::noise;
use crate::{probe::probe, ui::section};
use crate::{
probe::probe,
ui::{noise, section},
};
pub fn flows(cx: &mut App) -> impl IntoElement + use<> {
let budget = SankeyChart::new(
+1 -2
View File
@@ -1,8 +1,7 @@
use ely_gpui_component::charts::{BoxPlot, Histogram, ViolinPlot, WaterfallChart};
use gpui::{App, IntoElement, ParentElement, Styled, div, px};
use super::cartesian::noise;
use crate::ui::section;
use crate::ui::{noise, section};
pub fn distributions(cx: &mut App) -> impl IntoElement + use<> {
let mut rng = noise(13);
+4 -2
View File
@@ -4,8 +4,10 @@ use ely_gpui_component::charts::{
use gpui::{App, IntoElement, ParentElement, Styled, div, px};
use jiff::civil::date;
use super::cartesian::noise;
use crate::{probe::probe, ui::section};
use crate::{
probe::probe,
ui::{noise, section},
};
pub fn pies(cx: &mut App) -> impl IntoElement + use<> {
let traffic = [
+223
View File
@@ -0,0 +1,223 @@
use std::time::{Duration, Instant};
use ely_gpui_component::{
charts::PieChart,
data_display::Gauge,
feedback::Countdown,
finance::{
CryptoWalletCard, CurrencyConverter, Dividend, DividendTable, FinancialStatementTable,
PerformanceChart, PortfolioSummary, Settled, Statement, TransactionList, Transfer,
},
theme::TextSize,
typography::Caption,
};
use gpui::{App, IntoElement, ParentElement, SharedString, Styled, Window, div, px};
use jiff::{Timestamp, civil::date, tz::TimeZone};
use crate::ui::{keep, noise, row, section, set};
const HOLDINGS: [(&str, f64); 5] = [
("ELY", 48_210.0),
("SOLACE", 31_050.0),
("Bonds", 26_400.0),
("NOVA", 12_480.0),
("PINE", 8_196.0),
];
pub fn portfolio(cx: &mut App) -> impl IntoElement + use<> {
let cash = 9_864.0;
let worth = HOLDINGS.iter().map(|(_, worth)| worth).sum::<f64>() + cash;
let summary = HOLDINGS.iter().fold(
PortfolioSummary::new(
"portfolio",
worth,
((1_284.50, 0.0092), (18_402.3, 0.1553)),
cash,
"USD",
),
|summary, (name, worth)| summary.holding(*name, *worth),
);
let allocation = HOLDINGS
.iter()
.fold(PieChart::new("allocation"), |pie, (name, worth)| {
pie.slice(*name, *worth)
})
.slice("Cash", cash)
.donut("$136 K", "Invested");
let months = [
"Oct", "Nov", "Dec", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep",
];
let mut rng = noise(83);
let (mut equity, mut index) = (100.0, 100.0);
let (equity, index): (Vec<f64>, Vec<f64>) = months
.iter()
.map(|_| {
equity *= 1.0 + (rng() - 0.38) * 0.07;
index *= 1.0 + (rng() - 0.42) * 0.04;
(equity, index)
})
.unzip();
section(
"PortfolioSummary / AssetAllocationChart / PerformanceChart / SentimentGauge",
"What the portfolio is worth and how it moved, split across holdings; the same split as a donut; its growth against the market with each fall from a high below; and the market's mood on a dial.",
cx,
)
.child(summary)
.child(
div()
.flex()
.gap_8()
.items_center()
.child(div().w(px(420.)).child(allocation))
.child(Gauge::new("sentiment", "Sentiment", 64.0, 0.0, 100.0).decimals(0)),
)
.child(PerformanceChart::new("performance", months, equity).benchmark("EGX 500", index))
}
pub fn statements(cx: &mut App) -> impl IntoElement + use<> {
let dividends = [
(date(2025, 11, 7), date(2025, 11, 14), 0.24),
(date(2026, 2, 6), date(2026, 2, 13), 0.24),
(date(2026, 5, 8), date(2026, 5, 15), 0.26),
(date(2026, 8, 7), date(2026, 8, 14), 0.26),
]
.map(|(ex, pay, amount)| Dividend {
ex,
pay,
amount,
dividend_yield: amount * 4.0 / 187.25,
frequency: "Quarterly".into(),
});
let line = |name: &str, values: [f64; 4]| Statement::new(name.to_string(), values);
let lines = [
line("Revenue", [18_420.0, 19_106.0, 20_388.0, 21_950.0]).lines([
line("Products", [11_210.0, 11_380.0, 11_902.0, 12_430.0]),
line("Services", [7_210.0, 7_726.0, 8_486.0, 9_520.0]),
]),
line("Cost of revenue", [-7_920.0, -8_120.0, -8_480.0, -8_940.0]),
line(
"Operating expenses",
[-5_880.0, -6_010.0, -6_240.0, -6_530.0],
)
.lines([
line("Research", [-3_120.0, -3_210.0, -3_360.0, -3_520.0]),
line("Sales and admin", [-2_760.0, -2_800.0, -2_880.0, -3_010.0]),
]),
line("Operating income", [4_620.0, 4_976.0, 5_668.0, 6_480.0]),
line("Net income", [3_710.0, 3_990.0, 4_560.0, 5_190.0]),
];
section(
"DividendTable / FinancialStatementTable",
"Dividends by date with their yield, and a statement by quarter whose lines open into their parts.",
cx,
)
.child(DividendTable::new("dividends", dividends, "USD"))
.child(FinancialStatementTable::new(
"statement",
["Q4 2025", "Q1 2026", "Q2 2026", "Q3 2026"],
lines,
))
}
/// Demo rates in US dollars per unit; codes outside the table get a steady made-up rate.
fn dollars(code: &str) -> f64 {
match code {
"USD" => 1.0,
"EUR" => 1.09,
"GBP" => 1.27,
"CHF" => 1.13,
"CAD" => 0.73,
"AUD" => 0.66,
"SGD" => 0.75,
"HKD" => 0.128,
"CNY" => 0.14,
"INR" => 0.012,
"JPY" => 0.0067,
"KRW" => 0.00073,
_ => {
let seed = code
.bytes()
.fold(7, |seed: u64, byte| seed * 31 + u64::from(byte));
0.001 + noise(seed)() * 0.5
}
}
}
pub fn money(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let exchange = keep(
"finance-exchange",
|| {
(
1_000.0,
SharedString::from("USD"),
SharedString::from("EUR"),
)
},
window,
cx,
);
let (amount, from, to) = exchange.read(cx).clone();
let store = exchange.clone();
let candle = keep(
"finance-candle",
|| Instant::now() + Duration::from_secs(222),
window,
cx,
);
let until = *candle.read(cx);
let restart = candle.clone();
let transfers = [
(15, 40, "Salary", 6_250.0, "USD", Settled::Done),
(14, 5, "Card ending 4417", -86.40, "USD", Settled::Pending),
(11, 52, "Wallet bc1q…yg3c", 0.0421, "BTC", Settled::Done),
(9, 18, "Rent", -2_400.0, "USD", Settled::Done),
(8, 2, "Exchange withdrawal", -500.0, "EUR", Settled::Failed),
]
.map(|(hour, minute, party, amount, unit, settled)| Transfer {
time: Timestamp::from_second(1_790_294_400 + hour * 3_600 + minute * 60).expect("a time"),
party: party.into(),
amount,
unit: unit.into(),
settled,
});
section(
"CurrencyConverter / CryptoWalletCard / TransactionList / Candle Countdown",
"An amount in another currency, at an illustrative rate; a wallet with its balance and an address to scan; transfers in and out with whether each settled; and the time left in the current candle.",
cx,
)
.child(
div()
.flex()
.gap_8()
.child(
div().w(px(460.)).child(
CurrencyConverter::new("converter", amount, (&from, &to), dollars(&from) / dollars(&to))
.on_change(move |amount, from, to, _, cx| set(&store, (amount, from, to), cx)),
),
)
.child(div().flex_1().child(CryptoWalletCard::new(
"wallet",
("Savings", "Bitcoin"),
(0.8412, "BTC"),
(54_310.22, "USD"),
"bc1q9h7garjy5kd5nwmrq2z6yufl0yq0p0a9w6yg3c",
))),
)
.child(
div()
.flex()
.gap_8()
.child(div().flex_1().child(TransactionList::new(transfers).zone(TimeZone::UTC)))
.child(
div()
.w(px(220.))
.flex()
.flex_col()
.gap_2()
.child(Caption::new("5m candle closes in"))
.child(row().child(Countdown::new("candle", until).size(TextSize::Xl).on_done(
move |_, cx| set(&restart, Instant::now() + Duration::from_secs(300), cx),
))),
),
)
}
+166
View File
@@ -0,0 +1,166 @@
use ely_gpui_component::{
finance::{
BidAskBar, DomLadder, Level2Quotes, OrderBook, PriceChangeBadge, PriceText, QuoteCard,
Side, SpreadIndicator, TickerTape, TimeAndSales, Trade,
},
theme::TextSize,
typography::Caption,
};
use gpui::{App, IntoElement, ParentElement, SharedString, Styled, Window, div, px};
use jiff::{Timestamp, tz::TimeZone};
use super::markets::day;
use crate::ui::{keep, noise, row, section, set};
/// Prices, each with the size waiting there.
type Levels = Vec<(f64, f64)>;
/// A book around a price: bids below it and asks above, a tick apart, sizes drawn steadily.
fn levels(seed: u64, middle: f64, tick: f64, count: usize) -> (Levels, Levels) {
let mut rng = noise(seed);
let bids = (0..count)
.map(|ix| {
(
middle - tick * (ix as f64 + 1.0),
(120.0 + rng() * 800.0 + ix as f64 * 60.0).round(),
)
})
.collect();
let asks = (0..count)
.map(|ix| {
(
middle + tick * (ix as f64 + 1.0),
(120.0 + rng() * 800.0 + ix as f64 * 55.0).round(),
)
})
.collect();
(bids, asks)
}
pub fn quotes(cx: &mut App) -> impl IntoElement + use<> {
let tape = [
("ELY", 187.24, 0.0124),
("NOVA", 62.10, -0.0081),
("ORBIT", 131.52, 0.0203),
("PINE", 27.33, -0.0152),
("QUILL", 88.05, 0.0041),
("RIVER", 45.60, 0.0),
("SOLACE", 312.90, 0.0077),
("TERRA", 9.84, -0.0312),
]
.iter()
.fold(TickerTape::new("tape"), |tape, (symbol, now, share)| {
tape.quote(*symbol, *now, *share)
});
let cards = [
("ELY", "Ely Systems", 41, 184.9),
("NOVA", "Nova Energy", 43, 62.6),
("ORBIT", "Orbit Freight", 47, 128.9),
]
.map(|(symbol, name, seed, close)| {
let line = day(seed, close);
let now = f64::from(*line.last().expect("a day"));
div().flex_1().child(QuoteCard::new(
SharedString::from(format!("card-{symbol}")),
(symbol, name),
(now, close),
line,
))
});
section(
"Ticker / TickerTape / QuoteCard / PriceText / PriceChangeBadge",
"Quotes running past in a band, and symbols at a glance: the price flashes green or red as it moves, and the badge reads the move since the last close.",
cx,
)
.child(div().w_full().child(tape))
.child(div().flex().gap_4().children(cards))
.child(
row()
.child(PriceText::new("price-up", 187.26).from(187.24).size(TextSize::Lg))
.child(PriceText::new("price-down", 62.08).from(62.10).size(TextSize::Lg))
.child(PriceChangeBadge::new(184.9, 187.26))
.child(PriceChangeBadge::new(62.6, 62.08))
.child(PriceChangeBadge::new(187.26, 187.26)),
)
}
pub fn book(cx: &mut App) -> impl IntoElement + use<> {
let (bids, asks) = levels(51, 187.25, 0.05, 12);
let (bid_size, ask_size) = (
bids.iter().map(|level| level.1).sum::<f64>(),
asks.iter().map(|level| level.1).sum::<f64>(),
);
let mut rng = noise(61);
let venues = ["NYSE", "ARCA", "BATS", "EDGX", "IEX", "NSDQ"];
let level2 = (0..10).fold(Level2Quotes::new(), |quotes, ix| {
let venue = venues[ix % venues.len()];
let step = (ix / 2) as f64 * 0.05;
quotes
.bid(venue, 187.20 - step, (100.0 + rng() * 900.0).round())
.ask(
venues[(ix + 3) % venues.len()],
187.30 + step,
(100.0 + rng() * 900.0).round(),
)
});
section(
"OrderBook / SpreadIndicator / BidAskBar / Level2 Quotes",
"Orders waiting on each side, best in the middle, each level shaded by its running total; the spread and the balance between sides; and each venue's quotes side by side.",
cx,
)
.child(
div()
.flex()
.gap_8()
.child(div().w(px(320.)).child(OrderBook::new("book", bids.clone(), asks.clone())))
.child(
div()
.flex_1()
.flex()
.flex_col()
.gap_6()
.child(SpreadIndicator::new(bids[0].0, asks[0].0))
.child(BidAskBar::new(bid_size, ask_size))
.child(level2),
),
)
}
pub fn tape(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let mut rng = noise(71);
let start = 1_790_342_400;
let trades: Vec<Trade> = (0..40)
.map(|ix| {
let buy = rng() > 0.45;
Trade {
time: Timestamp::from_second(start + ix * 7).expect("a time"),
price: 187.25 + (rng() - 0.5) * 0.4,
size: (rng().powi(3) * 5_000.0 + 20.0).round(),
side: if buy { Side::Buy } else { Side::Sell },
}
})
.collect();
let (bids, asks) = levels(81, 187.25, 0.05, 10);
let traded = keep("finance-traded", || None::<SharedString>, window, cx);
let (told, tell) = (traded.read(cx).clone(), traded.clone());
section(
"TimeAndSales / TradeTape / MarketDepthLadder / DOM",
"Trades as they print, newest on top and large prints bold; and the book as a ladder around the last price. Press a size to trade at its price.",
cx,
)
.child(
div()
.flex()
.gap_8()
.child(div().w(px(320.)).child(TimeAndSales::new("prints", trades).large(2_000.0).zone(TimeZone::UTC)))
.child(
div().flex_1().flex().flex_col().gap_2().child(
DomLadder::new("ladder", bids, asks, 187.25, 0.05).on_trade(move |side, at, _, cx| {
let words = format!("{} at {at:.2}", if side == Side::Buy { "Bid" } else { "Offer" });
set(&tell, Some(words.into()), cx)
}),
)
.children(told.map(|words| Caption::new(format!("Placed: {words}.")))),
),
)
}
+284
View File
@@ -0,0 +1,284 @@
use std::rc::Rc;
use ely_gpui_component::{
buttons::{Button, ButtonVariant},
finance::{
Arrangement, Candle, CandlestickChart, ChartKind, ChartSync, ChartTypeSwitcher, DepthChart,
Drawing, DrawingToolbar, IndicatorSelector, IntervalSelector, MultiChartLayout, Overlay,
PointFigureChart, RenkoChart, Study, TimeRangeSelector, Tool,
},
theme::ActiveTheme,
};
use gpui::{
App, AppContext as _, IntoElement, ParentElement, SharedString, Styled, Window, div, px,
};
use jiff::Timestamp;
use crate::{
probe::probe,
ui::{keep, noise, row, section, set},
};
/// A demo chart's width, inside the page's column.
pub(super) const WIDE: f32 = 840.0;
/// Today's prices every five minutes from the last close, moving as a quiet stock does.
pub(super) fn day(seed: u64, close: f64) -> Vec<f32> {
let mut rng = noise(seed);
let mut price = close;
(0..78)
.map(|_| {
price *= 1.0 + (rng() - 0.5) * 0.004;
price as f32
})
.collect()
}
/// A steady walk of candles `step` seconds apart from `start`, so captures repeat.
pub(super) fn walk(seed: u64, count: usize, start: f64, step: i64) -> Vec<Candle> {
let mut rng = noise(seed);
let first = 1_767_364_200;
let mut close = start;
(0..count)
.map(|ix| {
let open = close;
let drift = (rng() - 0.47) * 0.034;
close = (open * (1.0 + drift)).max(1.0);
let high = open.max(close) * (1.0 + rng() * 0.011);
let low = open.min(close) * (1.0 - rng() * 0.011);
let volume = 1.0e6 * (0.5 + rng()) * (1.0 + drift.abs() * 24.0);
let time = Timestamp::from_second(first + ix as i64 * step).expect("a time in range");
Candle::new(time, (open, high, low, close), volume)
})
.collect()
}
/// How many candles of an interval a range holds, and each candle's seconds.
fn span(range: &str, interval: &str) -> (usize, i64) {
let seconds = match interval {
"1m" => 60,
"5m" => 300,
"15m" => 900,
"1h" => 3_600,
"4h" => 14_400,
"1W" => 604_800,
"1M" => 2_592_000,
_ => 86_400,
};
let days = match range {
"1D" => 1.0,
"5D" => 5.0,
"1M" => 22.0,
"3M" => 63.0,
"6M" => 126.0,
"YTD" => 180.0,
"1Y" => 252.0,
"5Y" => 1_260.0,
_ => 2_520.0,
};
let per_day = (23_400.0 / seconds as f64).max(1.0 / (seconds as f64 / 86_400.0));
(((days * per_day) as usize).clamp(12, 2_400), seconds)
}
pub fn market(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let kind = keep("finance-kind", || ChartKind::Candles, window, cx);
let range = keep("finance-range", || SharedString::from("6M"), window, cx);
let interval = keep("finance-interval", || SharedString::from("1D"), window, cx);
let (now_kind, now_range, now_interval) = (
*kind.read(cx),
range.read(cx).clone(),
interval.read(cx).clone(),
);
let (count, step) = span(&now_range, &now_interval);
let seed = 7 + now_interval.len() as u64 * 13 + step as u64 % 97;
let candles = walk(seed, count, 184.0, step);
let alert = (candles.last().expect("a range holds candles").close * 1.04).round();
let (pick_kind, pick_range, pick_interval) = (kind.clone(), range.clone(), interval.clone());
section(
"CandlestickChart / OHLCChart / HeikinAshiChart / LineQuoteChart / VolumeChart / ChartTypeSwitcher / TimeRangeSelector / IntervalSelector",
"Prices as candles, bars, smoothed candles, a line or an area, with volume along the bottom and averages over the top. Drag or scroll sideways to move through time, hold Cmd and scroll to zoom, double-press to return to the newest.",
cx,
)
.child(
row()
.justify_between()
.w_full()
.child(
row()
.child(ChartTypeSwitcher::new("finance-kind", now_kind).on_change(move |kind, _, cx| set(&pick_kind, kind, cx)))
.child(IntervalSelector::new("finance-interval", now_interval.clone()).on_change(move |key, _, cx| set(&pick_interval, key.clone(), cx))),
)
.child(TimeRangeSelector::new("finance-range", now_range).on_change(move |key, _, cx| set(&pick_range, key.clone(), cx))),
)
.child(probe(
"market",
div().w(px(WIDE)).child(
CandlestickChart::new("market", candles)
.kind(now_kind)
.title(format!("ELY · {now_interval}"))
.volume()
.overlay(Overlay::Sma(20))
.overlay(Overlay::Ema(50))
.alert(alert, "Alert"),
),
))
}
pub fn indicators(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let chosen = keep(
"finance-indicators",
|| {
(
vec![Overlay::Bollinger(20, 2.0)],
vec![Study::Macd, Study::Rsi],
)
},
window,
cx,
);
let (overlays, studies) = chosen.read(cx).clone();
let candles = walk(11, 160, 92.0, 86_400);
let peer = walk(19, 160, 61.0, 86_400);
let chart = CandlestickChart::new("studies", candles)
.title("ELY · 1D")
.compare("PEER", peer);
let chart = overlays
.iter()
.fold(chart, |chart, overlay| chart.overlay(*overlay));
let chart = studies
.iter()
.fold(chart, |chart, study| chart.study(*study));
let picked = chosen.clone();
section(
"IndicatorOverlay / IndicatorPane / IndicatorSelector / CompareSymbol / PriceAlertLine",
"Bollinger bands over the prices, MACD and RSI in panes below, and a second symbol drawn from the same start so their moves compare. Choose indicators from the menu.",
cx,
)
.child(row().child(IndicatorSelector::new("finance-choose", overlays, studies).on_change(move |overlays, studies, _, cx| set(&picked, (overlays, studies), cx))))
.child(div().w_full().child(chart.alert(98.0, "Take profit")))
}
pub fn drawings(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let tool = keep("finance-tool", || Some(Tool::Trend), window, cx);
let marks = keep(
"finance-marks",
|| vec![Drawing::Fib((96.0, 118.0), (128.0, 142.0))],
window,
cx,
);
let (now_tool, now_marks) = (*tool.read(cx), marks.read(cx).clone());
let (pick, kept, cleared) = (tool.clone(), marks.clone(), marks.clone());
section(
"DrawingTools / DrawingToolbar",
"Pick a tool and drag over the prices: a trend line, a level, a box, or retracement levels between a swing's two ends. The pointer moves through time again.",
cx,
)
.child(
row()
.child(DrawingToolbar::new("finance-tools", now_tool).on_change(move |tool, _, cx| set(&pick, tool, cx)))
.child(Button::new("finance-clear", "Clear").variant(ButtonVariant::Ghost).on_click(move |_, _, cx| set(&cleared, Vec::new(), cx))),
)
.child(probe(
"drawings",
div().w(px(WIDE)).child(
CandlestickChart::new("drawn", walk(23, 140, 120.0, 86_400))
.title("ELY · 1D")
.drawings(now_marks)
.tool(now_tool)
.on_draw(move |drawing, _, cx| {
kept.update(cx, |marks, cx| {
marks.push(drawing);
cx.notify();
})
}),
),
))
}
pub fn depth(cx: &mut App) -> impl IntoElement + use<> {
let mut rng = noise(29);
let bids: Vec<(f64, f64)> = (0..40)
.map(|ix| {
(
187.20 - ix as f64 * 0.05,
200.0 + rng() * 900.0 + ix as f64 * 40.0,
)
})
.collect();
let asks: Vec<(f64, f64)> = (0..40)
.map(|ix| {
(
187.26 + ix as f64 * 0.05,
200.0 + rng() * 900.0 + ix as f64 * 35.0,
)
})
.collect();
section("DepthChart", "Every bid and ask as a running total out from the spread. Hover to read the total to any price.", cx)
.child(probe("depth", div().w(px(WIDE)).child(DepthChart::new("depth", bids, asks))))
}
pub fn profile(cx: &mut App) -> impl IntoElement + use<> {
section(
"MarketProfile / VolumeProfile",
"How much traded at each price in view, as bars from the right edge; the busiest price is the darkest.",
cx,
)
.child(div().w_full().child(CandlestickChart::new("profile", walk(31, 120, 54.0, 86_400)).title("ELY · 1D").profile()))
}
pub fn bricks(cx: &mut App) -> impl IntoElement + use<> {
let candles = Rc::new(walk(37, 260, 74.0, 86_400));
section(
"RenkoChart / PointAndFigure",
"Time drops out: a brick for each whole move of two dollars, and columns of Xs and Os that turn after three boxes the other way.",
cx,
)
.child(
div()
.flex()
.gap_6()
.child(div().flex_1().child(RenkoChart::new("renko", candles.clone(), 2.0)))
.child(div().flex_1().child(PointFigureChart::new("figure", candles, 2.0, 3))),
)
}
pub fn together(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let arrangement = keep("finance-arrangement", || Arrangement::Four, window, cx);
let sync = keep(
"finance-sync",
|| None::<gpui::Entity<ChartSync>>,
window,
cx,
);
if sync.read(cx).is_none() {
let made = cx.new(|_| ChartSync::default());
set(&sync, Some(made), cx);
}
let shared = sync.read(cx).clone().expect("made above");
let now = *arrangement.read(cx);
let pick = arrangement.clone();
let names = [
("ELY", 3, 184.0),
("NOVA", 5, 62.0),
("ORBIT", 8, 131.0),
("PINE", 13, 27.0),
];
let layout = names.iter().fold(
MultiChartLayout::new("finance-grid", now),
|layout, (name, seed, start)| {
let chart = CandlestickChart::new(
SharedString::from(format!("sync-{name}")),
walk(*seed, 120, *start, 86_400),
)
.title(*name)
.sync(&shared);
layout.cell(chart.h(cx.theme().chart().height * 0.8))
},
);
section(
"MultiChartLayout / ChartSync",
"Four symbols moving together: pan, zoom or point at one and the others follow. Switch between one chart, two across, two down or four.",
cx,
)
.child(layout.on_arrange(move |arrangement, _, cx| set(&pick, arrangement, cx)))
}
+74
View File
@@ -0,0 +1,74 @@
mod assets;
mod book;
mod markets;
mod overview;
mod trading;
use gpui::{AnyElement, App, IntoElement, ParentElement, Window, div};
use super::Page;
use crate::script::Step;
pub const PAGE: Page = Page {
number: 16,
slug: "finance",
title: "Finance",
summary: "Market charts with the studies traders read, books, quotes, order entry and portfolios.",
render,
script: SCRIPT,
};
const SCRIPT: &[Step] = &[
Step::Rest,
Step::HoverAt("market", 520.0, 160.0),
Step::Wait(300),
Step::Shot("crosshair"),
Step::DownAt("drawings", 240.0, 210.0),
Step::DragTo("drawings", 620.0, 90.0),
Step::UpAt("drawings", 620.0, 90.0),
Step::Wait(300),
Step::Shot("drawn"),
Step::HoverAt("depth", 300.0, 150.0),
Step::Wait(300),
Step::Shot("depth"),
Step::HoverAt("payoff", 560.0, 120.0),
Step::Wait(300),
Step::Shot("payoff"),
Step::HoverAt("heatmap", 150.0, 150.0),
Step::Wait(300),
Step::Shot("heatmap"),
Step::DownAt("ticket", 170.0, 256.0),
Step::UpAt("ticket", 170.0, 256.0),
Step::Wait(400),
Step::Shot("confirm"),
Step::Key("escape"),
Step::Wait(300),
Step::Rest,
];
fn render(window: &mut Window, cx: &mut App) -> AnyElement {
div()
.child(markets::market(window, cx))
.child(markets::indicators(window, cx))
.child(markets::drawings(window, cx))
.child(markets::depth(cx))
.child(markets::profile(cx))
.child(markets::bricks(cx))
.child(markets::together(window, cx))
.child(book::quotes(cx))
.child(book::book(cx))
.child(book::tape(window, cx))
.child(trading::ticket(window, cx))
.child(trading::blotter(cx))
.child(trading::risk(window, cx))
.child(trading::options(window, cx))
.child(overview::watch(window, cx))
.child(overview::heatmap(cx))
.child(overview::screener(window, cx))
.child(overview::overview(cx))
.child(overview::calendars(window, cx))
.child(assets::portfolio(cx))
.child(assets::statements(cx))
.child(assets::money(window, cx))
.into_any_element()
}
+405
View File
@@ -0,0 +1,405 @@
use ely_gpui_component::{
data_display::Tone,
finance::{
Earnings, EarningsCalendar, EconomicCalendar, MarketHeatmap, MarketOverview, MarketStatus,
NewsFeed, Release, Screener, Session, Story, SymbolBadge, SymbolSearch,
TradingSessionClock, Watch, Watchlist,
},
forms::TextInput,
tables::{Cell, Column, FilterRule, Row, Test},
typography::Caption,
};
use gpui::{App, IntoElement, ParentElement, SharedString, Styled, Window, div, px};
use jiff::{Timestamp, civil::date, tz::TimeZone};
use super::markets::day;
use crate::{
probe::probe,
ui::{keep, row, section, set},
};
/// The demo's symbols: letters, name, where each trades, last close, and a seed for its day.
const SYMBOLS: [(&str, &str, &str, f64, u64); 8] = [
("ELY", "Ely Systems", "NASDAQ", 184.9, 41),
("NOVA", "Nova Energy", "NYSE", 62.6, 43),
("ORBIT", "Orbit Freight", "NYSE", 128.9, 47),
("PINE", "Pine Foods", "NASDAQ", 27.7, 53),
("QUILL", "Quill Media", "NASDAQ", 87.7, 59),
("RIVER", "River Health", "NYSE", 45.2, 67),
("SOLACE", "Solace Labs", "NASDAQ", 310.5, 73),
("TERRA", "Terra Mining", "LSE", 10.1, 79),
];
fn watches() -> Vec<Watch> {
SYMBOLS
.iter()
.map(|(symbol, name, _, close, seed)| {
let day = day(*seed, *close);
Watch {
symbol: (*symbol).into(),
name: (*name).into(),
last: f64::from(*day.last().expect("a day")),
close: *close,
day,
}
})
.collect()
}
pub fn watch(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let chosen = keep("finance-watched", || SharedString::from("ELY"), window, cx);
let field = window.use_keyed_state("finance-symbol", cx, |window, cx| {
TextInput::new(window, cx).placeholder("Search symbols")
});
let (now, pick, find) = (chosen.read(cx).clone(), chosen.clone(), chosen.clone());
let symbols = SYMBOLS.iter().map(|(symbol, name, venue, ..)| {
(
SharedString::from(*symbol),
SharedString::from(*name),
SharedString::from(*venue),
)
});
let badges = SYMBOLS.iter().take(4).map(|(symbol, _, venue, ..)| {
SymbolBadge::new(SharedString::from(format!("badge-{symbol}")), *symbol).venue(*venue)
});
section(
"Watchlist / SymbolBadge / SymbolSearch",
"Symbols to follow, each with its day as a line, its price and its move; a symbol as a tile of its letters; and a search by letters or name.",
cx,
)
.child(
div()
.flex()
.gap_8()
.child(
div().w(px(520.)).child(
Watchlist::new("watchlist", watches())
.selected(now.clone())
.on_select(move |symbol, _, cx| set(&pick, symbol.clone(), cx)),
),
)
.child(
div()
.flex_1()
.flex()
.flex_col()
.gap_4()
.child(
SymbolSearch::new("symbol-search", &field, symbols)
.on_pick(move |symbol, _, cx| set(&find, symbol.clone(), cx)),
)
.child(row().children(badges))
.child(Caption::new(format!("Watching {now}."))),
),
)
}
pub fn heatmap(cx: &mut App) -> impl IntoElement + use<> {
let tiles = [
("SOLACE", 920.0, 0.0212),
("ELY", 780.0, 0.0124),
("ORBIT", 410.0, -0.0203),
("NOVA", 380.0, -0.0081),
("QUILL", 260.0, 0.0041),
("RIVER", 240.0, 0.0),
("PINE", 150.0, -0.0152),
("TERRA", 120.0, -0.0312),
("ATLAS", 110.0, 0.0088),
("BIRCH", 90.0, 0.0263),
("CEDAR", 70.0, -0.0047),
("DUNE", 60.0, 0.0151),
];
let map = tiles.iter().fold(
MarketHeatmap::new("heatmap"),
|map, (symbol, weight, change)| map.tile(*symbol, *weight, *change),
);
section(
"MarketHeatmap / SectorTreemap",
"Each symbol a tile sized by its market value and tinted by its move: deeper green as it rises, deeper red as it falls. Hover a tile to read it.",
cx,
)
.child(probe("heatmap", div().w(px(840.)).child(map.h(px(320.)))))
}
pub fn screener(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let rules = keep(
"finance-screen",
|| {
(
vec![FilterRule {
column: "change".into(),
test: Test::Above,
value: "0".into(),
}],
false,
)
},
window,
cx,
);
let (now, any) = rules.read(cx).clone();
let store = rules.clone();
let columns = [
Column::new("symbol", "Symbol"),
Column::new("sector", "Sector"),
Column::new("last", "Last").end().decimals(2),
Column::new("change", "Change")
.end()
.decimals(2)
.suffix("%"),
Column::new("value", "Value").end().decimals(1).suffix(" B"),
Column::new("earnings", "P/E").end().decimals(1),
];
let sectors = [
"Software",
"Energy",
"Transport",
"Food",
"Media",
"Health",
"Software",
"Materials",
];
let rows = SYMBOLS.iter().zip(watches()).zip(sectors).enumerate().map(
|(ix, (((symbol, ..), watch), sector))| {
let change = (watch.last / watch.close - 1.0) * 100.0;
Row::new(
*symbol,
[
Cell::Text((*symbol).into()),
Cell::Tag(sector.into(), Tone::Neutral),
Cell::Number(watch.last),
Cell::Number(change),
Cell::Number(12.0 + ix as f64 * 17.5),
Cell::Number(9.0 + (ix * 7 % 30) as f64),
],
)
},
);
section(
"Screener / FilterPanel",
"Securities narrowed by rules: build the rules above, and the table keeps only what passes them.",
cx,
)
.child(
Screener::new("screener", columns, rows)
.rules(now, any)
.on_rules(move |rules, any, _, cx| set(&store, (rules.to_vec(), any), cx)),
)
}
pub fn overview(cx: &mut App) -> impl IntoElement + use<> {
let indexes = [
("EGX 500", "Broad market", 5_812.4, 41),
("EGX Tech", "Technology", 19_440.1, 43),
("EGX Small", "Small companies", 2_231.8, 47),
];
let movers = [
("BIRCH", 18.92, 18.43),
("SOLACE", 317.08, 310.5),
("TERRA", 9.78, 10.1),
("ORBIT", 126.28, 128.9),
];
let board = indexes.iter().fold(
MarketOverview::new("overview")
.status(MarketStatus::new("New York", Session::Open).next("Closes in 2 h 14 min")),
|board, (symbol, name, close, seed)| {
let day = day(*seed, *close);
let last = f64::from(*day.last().expect("a day"));
board.index((*symbol, *name), (last, *close), day)
},
);
let board = movers.iter().fold(board, |board, (symbol, last, close)| {
board.mover(*symbol, *last, *close)
});
let clock = [
("Sydney", 23.0, 5.0),
("Tokyo", 0.0, 6.0),
("Hong Kong", 1.5, 8.0),
("Frankfurt", 7.0, 15.5),
("London", 8.0, 16.5),
("New York", 13.5, 20.0),
]
.iter()
.fold(
TradingSessionClock::new(15.75),
|clock, (name, open, close)| clock.market(*name, *open, *close),
);
section(
"MarketOverview / IndexCard / MarketStatus / TradingSessionClock",
"A market at a glance: its session, its indexes as cards, and the day's movers. Below, each market's hours across one day in UTC, those open now lit.",
cx,
)
.child(board)
.child(
row()
.gap_6()
.child(MarketStatus::new("London", Session::After).next("Opens in 15 h 15 min"))
.child(MarketStatus::new("Tokyo", Session::Closed).next("Opens in 8 h 15 min"))
.child(MarketStatus::new("Frankfurt", Session::Before).next("Opens in 30 min")),
)
.child(clock)
}
fn stamp(hour: i64, minute: i64) -> Timestamp {
Timestamp::from_second(1_790_294_400 + hour * 3_600 + minute * 60).expect("a time")
}
pub fn calendars(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let release = |(hour, minute),
region: &str,
event: &str,
weight,
figures: (Option<f64>, Option<f64>, Option<f64>),
unit: &str| Release {
time: stamp(hour, minute),
region: SharedString::from(region.to_string()),
event: SharedString::from(event.to_string()),
weight,
actual: figures.0,
forecast: figures.1,
previous: figures.2,
unit: SharedString::from(unit.to_string()),
};
let releases = [
release(
(1, 30),
"JP",
"Tokyo core consumer prices",
2,
(Some(2.4), Some(2.3), Some(2.2)),
"%",
),
release(
(8, 0),
"DE",
"Ifo business climate",
2,
(Some(87.1), Some(87.6), Some(87.3)),
"",
),
release(
(12, 30),
"US",
"Durable goods orders",
2,
(Some(0.8), Some(0.5), Some(-0.3)),
"%",
),
release(
(12, 30),
"US",
"Initial jobless claims",
3,
(None, Some(224.0), Some(231.0)),
"K",
),
release(
(14, 0),
"US",
"New home sales",
1,
(None, Some(668.0), Some(652.0)),
"K",
),
];
let earnings = [
(
date(2026, 9, 28),
"ELY",
"Ely Systems",
false,
1.84,
Some(1.97),
),
(
date(2026, 9, 29),
"NOVA",
"Nova Energy",
true,
0.62,
Some(0.55),
),
(
date(2026, 9, 30),
"SOLACE",
"Solace Labs",
false,
3.10,
None,
),
(date(2026, 10, 1), "QUILL", "Quill Media", true, 0.48, None),
]
.map(
|(date, symbol, name, before_open, estimate, reported)| Earnings {
date,
symbol: symbol.into(),
name: name.into(),
before_open,
estimate,
reported,
},
);
let story = |(hour, minute), source: &str, headline: &str, symbols: &[&str], lean| Story {
source: SharedString::from(source.to_string()),
time: stamp(hour, minute),
headline: SharedString::from(headline.to_string()),
symbols: symbols
.iter()
.map(|symbol| SharedString::from(symbol.to_string()))
.collect(),
lean,
};
let stories = [
story(
(15, 42),
"Wire",
"Ely Systems lifts its outlook as cloud orders climb",
&["ELY"],
Some(true),
),
story(
(15, 10),
"Markets Desk",
"Freight rates slide for a third week",
&["ORBIT", "TERRA"],
Some(false),
),
story(
(14, 36),
"Wire",
"Nova Energy names a new chief financial officer",
&["NOVA"],
None,
),
story(
(13, 58),
"Daily Brief",
"Solace Labs trial meets its main goal",
&["SOLACE"],
Some(true),
),
];
let opened = keep("finance-story", || None::<usize>, window, cx);
let (read, open) = (*opened.read(cx), opened.clone());
section(
"EconomicCalendar / EarningsCalendar / NewsFeed",
"The day's releases with how much each moves markets and its figure beside the forecast; reports due by date; and stories newest first, each dot leaning green or red. Press a story to open it.",
cx,
)
.child(EconomicCalendar::new(releases).zone(TimeZone::UTC))
.child(EarningsCalendar::new("earnings", earnings))
.child(
div()
.w(px(640.))
.flex()
.flex_col()
.gap_2()
.child(
NewsFeed::new("news", stories)
.zone(TimeZone::UTC)
.on_open(move |ix, _, cx| set(&open, Some(ix), cx)),
)
.children(read.map(|ix| Caption::new(format!("Opened story {}.", ix + 1)))),
)
}
+258
View File
@@ -0,0 +1,258 @@
use ely_gpui_component::{
finance::{
Fill, GreeksTable, Leg, LeverageSlider, MarginIndicator, OptionChain, OptionQuote, Order,
OrderConfirmDialog, OrderEntry, OrderKind, OrderTable, PayoffDiagram, PnLDisplay, Position,
PositionTable, QuickTradeButtons, RiskMeter, Side, Strike, TradeHistoryTable, Working,
},
typography::Caption,
};
use gpui::{App, IntoElement, ParentElement, SharedString, Styled, Window, div, px};
use jiff::{Timestamp, tz::TimeZone};
use super::markets::WIDE;
use crate::{
probe::probe,
ui::{keep, row, section, set},
};
/// The demo symbol's last trade.
const LAST: f64 = 187.25;
fn at(minutes: i64) -> Timestamp {
Timestamp::from_second(1_790_343_000 + minutes * 60).expect("a time")
}
pub fn ticket(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let order = keep(
"finance-order",
|| Order {
symbol: "ELY".into(),
side: Side::Buy,
kind: OrderKind::Limit,
quantity: 100.0,
price: 187.20,
},
window,
cx,
);
let asking = keep("finance-asking", || false, window, cx);
let told = keep("finance-told", || None::<SharedString>, window, cx);
let now = order.read(cx).clone();
let dialog = (*asking.read(cx)).then(|| {
let (shut, placed) = (asking.clone(), told.clone());
OrderConfirmDialog::new("finance-confirm", now.clone(), LAST, move |_, cx| {
set(&shut, false, cx)
})
.on_confirm(move |order, _, cx| {
set(
&placed,
Some(format!("Placed: {}.", order.words(2)).into()),
cx,
)
})
});
let caption = told
.read(cx)
.clone()
.unwrap_or("Nothing placed yet.".into());
let (store, ask, quick) = (order.clone(), asking.clone(), told.clone());
let entry = OrderEntry::new("ticket", now, LAST)
.on_change(move |order, _, cx| set(&store, order, cx))
.on_submit(move |_, _, cx| set(&ask, true, cx));
let buttons = QuickTradeButtons::new("quick", 187.24, 187.26).on_trade(move |side, _, cx| {
let words = match side {
Side::Buy => "Bought 100 ELY at 187.26.",
Side::Sell => "Sold 100 ELY at 187.24.",
};
set(&quick, Some(words.into()), cx)
});
section(
"OrderEntry / TradePanel / QuickTradeButtons / OrderConfirmDialog",
"An order filled in field by field, with what it comes to; its button asks before the order goes. Quick buttons trade at the bid or the ask in one press.",
cx,
)
.child(
div()
.flex()
.gap_8()
.child(probe("ticket", div().w(px(340.)).child(entry)))
.child(
div()
.flex_1()
.flex()
.flex_col()
.gap_4()
.child(buttons)
.child(Caption::new(caption)),
),
)
.children(dialog)
}
pub fn blotter(cx: &mut App) -> impl IntoElement + use<> {
let positions = [
("ELY", 300.0, 171.40, LAST),
("NOVA", -150.0, 64.80, 62.08),
("ORBIT", 80.0, 139.10, 131.52),
("PINE", 1_200.0, 25.90, 27.33),
]
.map(|(symbol, quantity, cost, last)| Position {
symbol: symbol.into(),
quantity,
cost,
last,
});
let order = |symbol: &str, side, kind, quantity, price| Order {
symbol: SharedString::from(symbol.to_string()),
side,
kind,
quantity,
price,
};
let working = [
(
4,
order("ELY", Side::Buy, OrderKind::Limit, 200.0, 185.50),
0.0,
),
(
11,
order("NOVA", Side::Sell, OrderKind::Stop, 150.0, 60.00),
0.0,
),
(
26,
order("QUILL", Side::Buy, OrderKind::Limit, 500.0, 87.80),
220.0,
),
]
.map(|(minutes, order, filled)| Working {
time: at(minutes),
order,
filled,
});
let fills = [
(2, "ELY", Side::Buy, 100.0, 186.92, 1.00),
(9, "PINE", Side::Buy, 400.0, 27.05, 1.60),
(17, "ORBIT", Side::Sell, 40.0, 132.40, 1.00),
(31, "ELY", Side::Buy, 200.0, 187.18, 1.20),
(44, "NOVA", Side::Sell, 150.0, 62.31, 1.00),
]
.map(|(minutes, symbol, side, quantity, price, fee)| Fill {
time: at(minutes),
symbol: symbol.into(),
side,
quantity,
price,
fee,
});
section(
"PositionTable / OrderTable / TradeHistoryTable / PnLDisplay",
"What is held and what it has made, orders still waiting with how much has filled, and each fill with its fee. Results read green for a gain and red for a loss.",
cx,
)
.child(
row()
.gap_12()
.child(PnLDisplay::new("pnl-day", "Today", (1_284.50, 0.0092), "USD"))
.child(PnLDisplay::new("pnl-open", "Open positions", (4_910.20, 0.0371), "USD"))
.child(PnLDisplay::new("pnl-month", "This month", (-2_046.75, -0.0148), "USD")),
)
.child(PositionTable::new("positions", positions))
.child(OrderTable::new("orders", working).zone(TimeZone::UTC))
.child(TradeHistoryTable::new("fills", fills).zone(TimeZone::UTC))
}
pub fn risk(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let leverage = keep("finance-leverage", || 5.0, window, cx);
let now = *leverage.read(cx);
let store = leverage.clone();
let used = 21_500.0 * now / 5.0;
section(
"LeverageSlider / MarginIndicator / RiskMeter",
"Leverage marked at the common steps; the margin it takes against the account, and the risk it carries. Each turns amber, then red, as it climbs. Drag the slider.",
cx,
)
.child(
div()
.flex()
.gap_12()
.items_center()
.child(
div()
.w(px(360.))
.flex()
.flex_col()
.gap_8()
.child(
LeverageSlider::new("leverage", now, 100.0)
.on_change(move |value, _, cx| set(&store, value, cx)),
)
.child(MarginIndicator::new("margin", used.min(96_000.0), 96_000.0)),
)
.child(RiskMeter::new("risk", (now * 0.9 + 8.0).min(100.0))),
)
}
pub fn options(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let spot = LAST;
let strikes = (0..9).map(|ix| {
let strike = 170.0 + ix as f64 * 5.0;
let (call, put) = ((spot - strike).max(0.0), (strike - spot).max(0.0));
let time = 3.2 - (strike - spot).abs() * 0.04;
let quote = |inner: f64, volume: f64, volatility: f64| OptionQuote {
bid: inner + time.max(0.3) - 0.05,
ask: inner + time.max(0.3) + 0.05,
volume,
volatility,
};
Strike {
strike,
call: quote(
call,
400.0 + ix as f64 * 120.0,
0.24 + (ix as f64 - 4.0).abs() * 0.01,
),
put: quote(
put,
1_400.0 - ix as f64 * 110.0,
0.26 + (ix as f64 - 4.0).abs() * 0.012,
),
}
});
let picked = keep("finance-picked", || None::<SharedString>, window, cx);
let (told, pick) = (picked.read(cx).clone(), picked.clone());
let spread = [
Leg {
call: true,
strike: 185.0,
quantity: 1.0,
premium: 4.10,
},
Leg {
call: true,
strike: 195.0,
quantity: -1.0,
premium: 1.05,
},
];
section(
"OptionChain / GreeksTable / PayoffDiagram",
"Calls to the left and puts to the right of each strike, the side in the money shaded and a line at the price; a position's greeks; and what a call spread pays at expiry. Hover the diagram to read any price.",
cx,
)
.child(
OptionChain::new("chain", strikes, spot).on_pick(move |strike, call, _, cx| {
let words = format!("{} {strike:.0}", if call { "Call" } else { "Put" });
set(&pick, Some(words.into()), cx)
}),
)
.children(told.map(|words| Caption::new(format!("Picked: {words}."))))
.child(div().w(px(640.)).child(GreeksTable::new([0.42, 0.031, -0.086, 0.121, 0.047])))
.child(probe(
"payoff",
div()
.w(px(WIDE))
.child(PayoffDiagram::new("payoff", spread, (165.0, 215.0), spot)),
))
}
+2
View File
@@ -2,6 +2,7 @@ mod buttons;
mod charts;
mod data;
mod feedback;
mod finance;
mod forms;
mod layout;
mod lists;
@@ -45,6 +46,7 @@ pub const ALL: &[Page] = &[
lists::PAGE,
tables::PAGE,
charts::PAGE,
finance::PAGE,
theme::PAGE,
];
+1 -12
View File
@@ -16,7 +16,7 @@ use gpui::{
use crate::{
probe::probe,
ui::{keep, section, set},
ui::{keep, noise, section, set},
};
pub fn plain(cx: &mut App) -> impl IntoElement + use<> {
@@ -49,17 +49,6 @@ const PEOPLE: [&str; 10] = [
];
const REGIONS: [&str; 4] = ["Europe", "Americas", "Asia", "Oceania"];
/// A steady pseudo-random series, so captures repeat.
fn noise(seed: u64) -> impl FnMut() -> f64 {
let mut state = seed;
move || {
state = state
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
(state >> 33) as f64 / (1u64 << 31) as f64
}
}
pub(super) fn order_rows() -> Vec<Row> {
let mut next = noise(7);
(0..24)
+12 -5
View File
@@ -1,8 +1,8 @@
use ely_gpui_component::{
theme::{ActiveTheme, Radius},
typography::{
Blockquote, Caption, Code, EllipsisTooltip, ExternalLink, Heading, Highlight, Kbd,
KbdCombo, Label, Link, MiddleEllipsis, Overline, Paragraph, Subtitle, Title,
Blockquote, Caption, Code, Ellipsis, EllipsisTooltip, ExternalLink, Heading, Highlight,
Kbd, KbdCombo, Label, Link, MiddleEllipsis, Overline, Paragraph, Subtitle, Title,
},
};
use gpui::{App, IntoElement, ParentElement, Styled, div, px};
@@ -149,11 +149,18 @@ fn narrow(cx: &App) -> gpui::Div {
pub fn truncate(cx: &App) -> impl IntoElement + use<> {
section(
"Truncate",
"gpui's .truncate(): one line, an ellipsis at the end.",
"Truncate / Ellipsis",
"One line, an ellipsis at the end, in the text style around it. It holds in flex boxes, where gpui's own .truncate() clips.",
cx,
)
.child(narrow(cx).child(div().truncate().child(PROSE)))
.child(
narrow(cx)
.flex()
.items_center()
.gap_2()
.child(div().flex_none().child("Note"))
.child(Ellipsis::new(PROSE)),
)
}
pub fn line_clamp(cx: &App) -> impl IntoElement + use<> {
+11
View File
@@ -94,3 +94,14 @@ pub fn set<T: 'static>(state: &Entity<T>, value: T, cx: &mut App) {
cx.notify();
});
}
/// A steady pseudo-random series, so captures repeat.
pub fn noise(seed: u64) -> impl FnMut() -> f64 {
let mut state = seed;
move || {
state = state
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
(state >> 33) as f64 / (1u64 << 31) as f64
}
}