The gallery's entry runs natively or, on wasm32, from a wasm_bindgen start through gpui_web, single-threaded with WebGL2 fallback. ?page, ?story and ?theme choose what shows; an unknown one fails in the page and the log. set_theme switches the mode later. Pictures load from ELY_GALLERY_ASSETS. scripts/stories.py writes examples/gallery/stories.json from the pages.
225 lines
7.6 KiB
Rust
225 lines
7.6 KiB
Rust
use std::time::Duration;
|
|
|
|
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 web_time::Instant;
|
|
|
|
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),
|
|
))),
|
|
),
|
|
)
|
|
}
|