use std::error::Error;
use std::sync::Arc;
use std::time::{Duration, Instant};
use arrow_array::{ArrayRef, Float64Array, Int64Array, RecordBatch, StringArray};
use densa_arrow::LiveModel;
use densa_data::{Filter, LiveTable};
use densa_grid::GridModel;
use densa_ui::{Action, ActionKind, Align, Style, Theme, Ui, WidgetId};
use densa_widgets::{DataGrid, Label, Panel, TextInput};
use densa_window::{App, Host, WindowOptions};
const INSTRUMENTS: usize = 100_000;
const UPDATES_PER_FRAME: usize = 1_000;
const STATUS_EVERY: Duration = Duration::from_millis(500);
const SECTORS: [&str; 8] = [
"Energy",
"Materials",
"Industrials",
"Utilities",
"Health",
"Financials",
"Technology",
"Telecom",
];
type Grid = DataGrid<LiveModel>;
struct Feed {
seed: u64,
}
impl Feed {
fn next(&mut self) -> u64 {
self.seed = self.seed.wrapping_add(0xA076_1D64_78BD_642F);
let t = u128::from(self.seed) * u128::from(self.seed ^ 0xE703_7ED1_A0B4_28DB);
(t >> 64) as u64 ^ t as u64
}
fn unit(&mut self) -> f64 {
(self.next() >> 11) as f64 / (1_u64 << 53) as f64
}
fn batch(
&mut self,
indices: impl Iterator<Item = usize>,
) -> Result<RecordBatch, Box<dyn Error>> {
let mut symbols = Vec::new();
let mut sectors = Vec::new();
let (mut last, mut change, mut volume) = (Vec::new(), Vec::new(), Vec::new());
for index in indices {
let open = open_price(index);
let move_pct = (self.unit() - 0.5) * 8.0;
symbols.push(format!("SYM{index:05}"));
sectors.push(SECTORS[index % SECTORS.len()]);
last.push(open * (1.0 + move_pct / 100.0));
change.push(move_pct);
volume.push((self.unit() * 10_000_000.0) as i64);
}
Ok(RecordBatch::try_from_iter([
("symbol", Arc::new(StringArray::from(symbols)) as ArrayRef),
("sector", Arc::new(StringArray::from(sectors)) as ArrayRef),
("last", Arc::new(Float64Array::from(last)) as ArrayRef),
("chg %", Arc::new(Float64Array::from(change)) as ArrayRef),
("volume", Arc::new(Int64Array::from(volume)) as ArrayRef),
])?)
}
}
fn open_price(index: usize) -> f64 {
5.0 + (index * 7_919 % 99_500) as f64 / 100.0
}
struct LiveApp {
light: bool,
close_at: Option<Instant>,
feed: Feed,
ids: Option<Ids>,
status_at: Instant,
}
struct Ids {
grid: WidgetId,
filter: WidgetId,
status: WidgetId,
}
impl LiveApp {
fn model(&mut self) -> Result<LiveModel, Box<dyn Error>> {
let first = self.feed.batch(0..INSTRUMENTS)?;
let mut table = LiveTable::new(first.schema(), "symbol")?;
table.upsert(first)?;
let mut model = LiveModel::new(table)?;
model.set_decimals(2, Some(2))?;
model.set_decimals(3, Some(2))?;
Ok(model)
}
fn show_status(ui: &mut Ui, ids: &Ids) {
let Some(grid) = ui.widget::<Grid>(ids.grid) else {
return;
};
let model = grid.model();
let shown = model.rows();
let total = model.table().len();
let selected = grid
.selected()
.and_then(|row| model.record(row))
.and_then(|record| model.table().row_of(record))
.and_then(|at| {
let symbols = model.table().batches()[at.batch].column(0);
let symbols = symbols.as_any().downcast_ref::<StringArray>()?;
Some(format!(", selected {}", symbols.value(at.row)))
})
.unwrap_or_default();
let text = format!("{total} instruments, {shown} shown{selected}");
ui.update::<Label, _>(ids.status, |label, cx| label.set_text(cx, text));
}
fn apply_filter(ui: &mut Ui, ids: &Ids) {
let text = ui
.widget::<TextInput>(ids.filter)
.map(|input| input.text().trim().to_uppercase())
.unwrap_or_default();
let filter = (!text.is_empty()).then(|| Filter::contains("symbol", text));
ui.update::<Grid, _>(ids.grid, |grid, cx| {
let _ = grid.model_mut(cx).set_filter(filter);
});
}
}
impl App for LiveApp {
fn build(&mut self, ui: &mut Ui, _: &mut Host<'_>) -> Result<(), Box<dyn Error>> {
if self.light {
ui.set_theme(Theme::light());
}
let root = ui.root();
let bar = Style::row()
.padding(8.0)
.gap(12.0)
.shrink(0.0)
.align_items(Align::Center);
let toolbar = ui.add(root, Panel::new(), bar)?;
ui.add(
toolbar,
Label::new("Live table").with_size(15.0),
Style::row(),
)?;
let filter = TextInput::new("").with_placeholder("Filter symbols");
let filter = ui.add(toolbar, filter, Style::row())?;
let grid = Grid::new(self.model()?).with_frozen_columns(1);
let grid = ui.add(root, grid, Grid::style())?;
let bar = Style::row().padding(6.0).shrink(0.0);
let bar = ui.add(root, Panel::new(), bar)?;
let status = ui.add(bar, Label::new("").dim(), Style::row())?;
let ids = Ids {
grid,
filter,
status,
};
Self::show_status(ui, &ids);
self.ids = Some(ids);
Ok(())
}
fn action(&mut self, ui: &mut Ui, _: &mut Host<'_>, action: Action) {
let Some(ids) = &self.ids else {
return;
};
if action.source == ids.filter && matches!(action.kind, ActionKind::Changed) {
Self::apply_filter(ui, ids);
}
Self::show_status(ui, ids);
}
fn frame(&mut self, ui: &mut Ui, host: &mut Host<'_>) -> bool {
let Some(ids) = &self.ids else {
return false;
};
let start = (self.feed.next() % INSTRUMENTS as u64) as usize;
let indices = (0..UPDATES_PER_FRAME).map(|i| (start + i * 97) % INSTRUMENTS);
if let Ok(batch) = self.feed.batch(indices) {
ui.update::<Grid, _>(ids.grid, |grid, cx| {
let _ = grid.model_mut(cx).table_mut().upsert(batch);
});
}
if self.status_at.elapsed() >= STATUS_EVERY {
self.status_at = Instant::now();
Self::show_status(ui, ids);
}
if self.close_at.is_some_and(|at| Instant::now() >= at) {
host.close();
return false;
}
true
}
}
fn main() -> Result<(), Box<dyn Error>> {
let args: Vec<String> = std::env::args().collect();
let light = args.iter().any(|a| a == "--light");
let seconds = args
.iter()
.skip_while(|a| *a != "--seconds")
.nth(1)
.map(|s| s.parse::<u64>())
.transpose()?;
let app = LiveApp {
light,
close_at: seconds.map(|s| Instant::now() + Duration::from_secs(s)),
feed: Feed { seed: 0x5EED },
ids: None,
status_at: Instant::now(),
};
densa_window::run(
WindowOptions::new("densa live table").with_size(1000.0, 700.0),
app,
)?;
Ok(())
}