Skip to main content

site_home/
lib.rs

1//! The RavenRS home page, built only from RavenRS components and drawn by RavenRS.
2//!
3//! On the web it starts from `web_entry` with WebGL2, so it opens in every browser
4//! and phone; `cargo run -p site-home` shows the same page in a window.
5
6use std::rc::Rc;
7
8use ravenrs::core::Key;
9use ravenrs::prelude::*;
10use ravenrs_report::{Document, ReportEngine, samples};
11use ravenrs_report_viewer::PageFan;
12
13/// Where the rest of the site lives, relative to the home page.
14const DEMO: &str = "demo/";
15const PLAYGROUND: &str = "playground/";
16const DESIGNER: &str = "designer/";
17const COMPONENTS: &str = "components/";
18const API: &str = "api/ravenrs/";
19const GITHUB: &str = "https://github.com/alexkads/RavenRS";
20const GUIDE: &str = "docs/getting-started/";
21/// Height of the bar pinned at the top; anchors land below it.
22const BAR: f32 = 64.0;
23
24/// The reports the fan shows, rendered once by the real engine.
25#[derive(Debug)]
26struct Reports {
27    customers: Document,
28    invoice: Document,
29    dashboard: Document,
30    sales: Document,
31}
32
33impl Reports {
34    fn render() -> Self {
35        let mut engine = ReportEngine::new();
36        let mut run = |report: ravenrs_report::Report| {
37            let data = report.embedded_data().cloned().unwrap_or(serde_json::Value::Null);
38            engine.render(&report, &data)
39        };
40        let customers = run(samples::customer_list());
41        let dashboard = run(samples::sales_dashboard());
42        let sales = run(samples::sales_by_region(300));
43        let invoice = engine.render(&samples::invoice(), &samples::invoice_data());
44        Self { customers, invoice, dashboard, sales }
45    }
46}
47
48/// The whole page.
49#[derive(Debug)]
50pub struct Home {
51    reports: Rc<Reports>,
52    palette: State<bool>,
53    platform: State<usize>,
54    clicks: State<u32>,
55}
56
57impl Default for Home {
58    fn default() -> Self {
59        Self::new()
60    }
61}
62
63impl Home {
64    /// The page, with its sample reports rendered.
65    pub fn new() -> Self {
66        Self {
67            reports: Rc::new(Reports::render()),
68            palette: State::new(false),
69            platform: State::new(0),
70            clicks: State::new(0),
71        }
72    }
73}
74
75impl View for Home {
76    fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
77        let links = cx.links();
78        let open = self.palette.clone();
79        cx.shortcut(Key::Character("k".into()), true, Rc::new(move || open.set(!open.get())));
80        let (reports, palette, platform, clicks) =
81            (self.reports.clone(), self.palette.clone(), self.platform.clone(), self.clicks.clone());
82        let page = responsive(move |size| -> AnyView {
83            let narrow = size.width < 760.0;
84
85            Box::new(ZStack::new((
86                ScrollView::new((
87                    // Room for the bar, which floats over the page.
88                    Spacer::new().frame(None, Some(BAR)),
89                    hero(size, narrow, links.clone()),
90                    Anchor::new("demo", demo(links.clone())).offset(BAR),
91                    Anchor::new("pipeline", pipeline(narrow, clicks.clone())).offset(BAR),
92                    Anchor::new("components", components(narrow)).offset(BAR),
93                    Anchor::new("reports", reporting(reports.clone(), narrow, links.clone())).offset(BAR),
94                    Anchor::new("platforms", platforms(narrow, platform.clone())).offset(BAR),
95                    Anchor::new("roadmap", roadmap(narrow)).offset(BAR),
96                    closing(links.clone()),
97                ))
98                .spacing(0.0)
99                .height(size.height),
100                nav(narrow, palette.clone()),
101                command_palette(palette.clone(), links.clone()),
102            )))
103        });
104        cx.child(&page)
105    }
106}
107
108/// A column that gives its children its whole width, so their text wraps.
109fn stack<C: ViewSeq>(children: C) -> VStack<C> {
110    VStack::new(children).align(Align::Stretch)
111}
112
113/// A handler that opens `url` through the links service.
114fn go(links: &Links, url: &'static str) -> impl Fn() + 'static {
115    let links = links.clone();
116    move || links.open(url)
117}
118
119/// A section's column: centered, at most 1160 pixels wide, with room on the sides.
120fn column<V: View + 'static>(content: V) -> impl View {
121    HStack::new((
122        Spacer::new(),
123        stack((content,)).frame(None, None).max_width(1160.0).grow(1.0),
124        Spacer::new(),
125    ))
126    .padding(Insets::symmetric(0.0, 20.0))
127}
128
129fn section<V: View + 'static>(content: V) -> impl View {
130    stack((column(content),)).padding(Insets::symmetric(96.0, 0.0))
131}
132
133fn kicker(text: &str) -> Badge {
134    Badge::new(text).tone(Tone::Accent)
135}
136
137fn heading(text: &str) -> Text {
138    Text::new(text).style(TextStyle::Display)
139}
140
141fn lede(text: &str) -> Text {
142    Text::new(text).foreground(Semantic::InkMuted)
143}
144
145fn nav(narrow: bool, palette: State<bool>) -> impl View {
146    let brand = HStack::new((
147        Icon::new(IconName::Layers).color(Semantic::Accent),
148        Text::new("RavenRS").style(TextStyle::Headline),
149        Badge::new("v27.1"),
150    ))
151    .spacing(10.0)
152    .align(Align::Center);
153    let links: AnyView = if narrow {
154        Box::new(Spacer::new())
155    } else {
156        Box::new(
157            HStack::new((
158                Link::new("Components", COMPONENTS).muted(),
159                Link::new("Playground", PLAYGROUND).muted(),
160                Link::new("Reports", "#reports").muted(),
161                Link::new("Docs", GUIDE).muted(),
162                Link::new("API", API).muted(),
163            ))
164            .spacing(4.0),
165        )
166    };
167    let search = Button::new(if narrow { "" } else { "Search" })
168        .icon(IconName::Search)
169        .style(ButtonStyle::Secondary)
170        .size(ButtonSize::Small)
171        .shortcut("⌘K")
172        .on_click(move || palette.set(true));
173    column(
174        HStack::new((brand, Spacer::new(), links, search, Link::new("GitHub", GITHUB).icon(IconName::Star)))
175            .spacing(8.0)
176            .align(Align::Center)
177            .frame(None, Some(BAR)),
178    )
179    .background(Semantic::Canvas)
180}
181
182/// The app screen the floor of squares stands up into: a sidebar, cards and a chart.
183fn screen(u: f32, v: f32, time: f64) -> Color {
184    let theme = Theme::raven_dark();
185    let c = |s: Semantic| theme.color(s);
186    let inside = |a: f32, b: f32, top: f32, bottom: f32| u >= a && u <= b && v >= top && v <= bottom;
187    if v < 0.075 {
188        return if inside(0.012, 0.028, 0.02, 0.06) {
189            c(Semantic::Danger)
190        } else if inside(0.036, 0.052, 0.02, 0.06) {
191            c(Semantic::Warning)
192        } else if inside(0.06, 0.076, 0.02, 0.06) {
193            c(Semantic::Success)
194        } else {
195            c(Semantic::Surface)
196        };
197    }
198    if u < 0.2 {
199        if inside(0.02, 0.18, 0.13, 0.2) {
200            return if inside(0.03, 0.05, 0.14, 0.19) { c(Semantic::Accent) } else { c(Semantic::Control) };
201        }
202        let row = [0.25, 0.33, 0.41, 0.49].iter().any(|t| inside(0.03, 0.15, *t, t + 0.03));
203        return if row { c(Semantic::Border) } else { c(Semantic::Surface) };
204    }
205    if inside(0.24, 0.48, 0.11, 0.15) {
206        return c(Semantic::Ink);
207    }
208    if inside(0.24, 0.42, 0.18, 0.2) {
209        return c(Semantic::InkMuted);
210    }
211    if inside(0.84, 0.96, 0.11, 0.18) {
212        return c(Semantic::Accent);
213    }
214    for (a, b) in [(0.24, 0.47), (0.495, 0.725), (0.75, 0.96)] {
215        if inside(a, b, 0.25, 0.43) {
216            return if inside(a + 0.02, a + 0.14, 0.32, 0.37) {
217                c(Semantic::Ink)
218            } else if inside(a + 0.02, a + 0.1, 0.28, 0.3) {
219                c(Semantic::InkMuted)
220            } else if inside(a + 0.02, a + 0.07, 0.39, 0.41) {
221                c(Semantic::Success)
222            } else {
223                c(Semantic::SurfaceElevated)
224            };
225        }
226    }
227    if inside(0.24, 0.7, 0.48, 0.94) {
228        if inside(0.27, 0.67, 0.53, 0.9) && (u - 0.27) % 0.04 < 0.028 {
229            let bar = ((u - 0.27) / 0.04).floor();
230            let top = 0.9 - 0.37 * (0.35 + 0.55 * (0.5 + 0.5 * (bar * 1.7 + time as f32 * 1.2).sin()));
231            if v >= top {
232                return c(Semantic::Accent);
233            }
234        }
235        return c(Semantic::SurfaceElevated);
236    }
237    if inside(0.725, 0.96, 0.48, 0.94) {
238        if inside(0.745, 0.94, 0.83, 0.9) {
239            return c(Semantic::Accent);
240        }
241        return c(Semantic::SurfaceElevated);
242    }
243    c(Semantic::Canvas)
244}
245
246fn hero(size: Size, narrow: bool, links: Links) -> impl View {
247    let height = (size.height - BAR).clamp(560.0, 920.0);
248    let (columns, rows) = if narrow { (72, 40) } else { (128, 72) };
249    let field = QuadField::new(columns, rows, screen).height(height);
250    let words = stack((
251        HStack::new((
252            kicker("Free and open source"),
253            lede("160+ components · desktop and WebGL from one codebase"),
254        ))
255        .spacing(10.0)
256        .align(Align::Center),
257        Text::new("Beautiful native interfaces, powered by Rust.").style(TextStyle::Display),
258        lede(
259            "Write your UI once, declaratively. RavenRS draws every pixel on the GPU: in a native window on \
260             Windows, macOS and Linux, and in the browser, as this page is.",
261        ),
262        HStack::new((
263            Button::new("Try the live demo")
264                .icon(IconName::Play)
265                .size(ButtonSize::Large)
266                .on_click(go(&links, DEMO)),
267            Button::new("Open the report designer")
268                .icon(IconName::FileText)
269                .style(ButtonStyle::Secondary)
270                .size(ButtonSize::Large)
271                .on_click(go(&links, DESIGNER)),
272        ))
273        .spacing(12.0)
274        .wrap(),
275        CodeBlock::new("npx create-ravenrs minha-loja"),
276    ))
277    .spacing(20.0)
278    .max_width(820.0)
279    .padding(Insets::symmetric(if narrow { 28.0 } else { 72.0 }, 20.0));
280    let fading = ScrollLinked::new(words, ScrollLink::fade_out().between(0.0, 0.3));
281    let caption = ScrollLinked::new(
282        Badge::new("Not a video: every square is drawn by RavenRS, every frame").tone(Tone::Accent),
283        ScrollLink::fade_in().between(0.8, 1.0),
284    );
285    ScrollScene::new(ZStack::new((
286        field,
287        stack((column(fading), Spacer::new(), stack((caption,)).align(Align::Center).padding(28.0)))
288            .frame(None, Some(height)),
289    )))
290    .length(height)
291    .top(BAR)
292}
293
294fn demo(links: Links) -> impl View {
295    section(
296        Card::new(
297            stack((
298                kicker("Live demo"),
299                heading("Every component, running in your browser."),
300                lede(
301                    "This page is a RavenRS app. The gallery is a bigger one: every component, the themes, a \
302                     100,000-row data grid, typing, dragging and the keyboard.",
303                ),
304                Button::new("Open the gallery").icon(IconName::Play).on_click(go(&links, DEMO)),
305            ))
306            .spacing(14.0),
307        )
308        .padding(32.0),
309    )
310}
311
312const STEPS: [(&str, &str); 6] = [
313    ("State", "A click changes a value. RavenRS knows exactly which views read it."),
314    ("View", "Plain Rust builders describe the tree. No macros required."),
315    ("Layout", "Flex and grid solve sizes and positions for what changed."),
316    ("Text", "Shaped, wrapped and drawn from a glyph atlas at your pixel density."),
317    ("Paint", "One SDF shader draws corners, borders, gradients and shadows."),
318    ("Present", "The GPU presents the frame: Metal, DX12, Vulkan, WebGPU or WebGL2."),
319];
320
321const CODE: [&str; 6] = [
322    "let count = State::new(0);\nlet inc = count.clone();\n\n// Readers are tracked. Only they redraw.\nButton::new(\"Add one\")\n    .icon(IconName::Plus)\n    .on_click(move || inc.update(|n| *n += 1))",
323    "stack((\n    Text::new(\"Welcome\").style(TextStyle::Display),\n    Card::new(Text::bound(&count, |n| format!(\"Clicks: {n}\"))),\n    Button::new(\"Add one\").on_click(on_inc),\n))\n.spacing(16.0)",
324    "// Flex and grid, solved once per change.\nHStack::new((sidebar, content.grow(1.0), inspector))\n    .spacing(16.0)\n    .padding(32.0)",
325    "// Parley shapes and wraps; swash rasterizes into a GPU atlas.\nText::new(\"RavenRS\").style(TextStyle::Title)",
326    "// Corners, borders, gradients and soft shadows.\nCard::new(body)\n    .corner_radius(24.0)\n    .shadow(Shadow::soft())",
327    "// Only the window and input glue differ.\nfn main() {\n    ravenrs::run(\"Counter\", app);   // desktop\n}\n// scripts/build-web.sh counter  → browser",
328];
329
330/// The share of the scene each step holds, with a short cross-fade into the next.
331fn step_link(i: usize, opacity_off: f32) -> ScrollLink {
332    let n = STEPS.len() as f32;
333    let (from, to) = (i as f32 / n, (i + 1) as f32 / n);
334    let off = Effect::IDENTITY.with_opacity(opacity_off);
335    let mut stops = vec![];
336    if i > 0 {
337        stops.push((from - 0.04, off));
338    }
339    stops.push((from + 0.02, Effect::IDENTITY));
340    stops.push((to - 0.02, Effect::IDENTITY));
341    if i + 1 < STEPS.len() {
342        stops.push((to + 0.04, off));
343    }
344    ScrollLink::keyframes(stops)
345}
346
347fn step_visual(i: usize, clicks: &State<u32>) -> AnyView {
348    match i {
349        0 => {
350            let (shown, inc) = (clicks.clone(), clicks.clone());
351            Box::new(
352                Card::new(
353                    HStack::new((
354                        stack((
355                            Text::new("CLICKS").style(TextStyle::Caption).foreground(Semantic::InkMuted),
356                            Text::dynamic(move || shown.get().to_string()).style(TextStyle::Metric),
357                        ))
358                        .spacing(4.0),
359                        Button::new("Add one").icon(IconName::Plus).on_click(move || inc.update(|n| *n += 1)),
360                    ))
361                    .spacing(24.0)
362                    .align(Align::Center),
363                )
364                .padding(24.0),
365            )
366        }
367        1 => Box::new(
368            stack((
369                Badge::new("VStack").tone(Tone::Accent),
370                HStack::new((Badge::new("Text"), Badge::new("Card"), Badge::new("Button"))).spacing(10.0),
371                Badge::new("Text::bound(count)"),
372            ))
373            .spacing(14.0)
374            .align(Align::Center),
375        ),
376        2 => Box::new(
377            HStack::new((
378                Card::new(Text::new("Fixed")).padding(20.0),
379                Card::new(Text::new("Grows")).padding(20.0).grow(1.0),
380                Card::new(Text::new("Hugs")).padding(20.0),
381            ))
382            .spacing(10.0),
383        ),
384        3 => Box::new(Text::new("RavenRS · Inter · 16 px").style(TextStyle::Metric)),
385        4 => Box::new(Card::new(Stat::new("Revenue", "R$ 128.450").delta("+12.4%", true)).padding(24.0)),
386        _ => Box::new(
387            stack((
388                ProgressBar::new(1.0).label("Metal · macOS"),
389                ProgressBar::new(1.0).label("DX12 · Windows"),
390                ProgressBar::new(1.0).label("Vulkan · Linux"),
391                ProgressBar::new(1.0).label("WebGPU or WebGL2 · browser").tone(Tone::Success),
392            ))
393            .spacing(12.0)
394            .align(Align::Stretch),
395        ),
396    }
397}
398
399fn pipeline(narrow: bool, clicks: State<u32>) -> impl View {
400    let steps: Vec<AnyView> = STEPS
401        .iter()
402        .enumerate()
403        .map(|(i, (title, text))| {
404            let title =
405                HStack::new((Badge::new((i + 1).to_string()), Text::new(*title).style(TextStyle::Headline)))
406                    .spacing(10.0)
407                    .align(Align::Center);
408            // On a phone the panel and the list share the screen: titles only.
409            let row: AnyView =
410                if narrow { Box::new(title) } else { Box::new(stack((title, lede(text))).spacing(4.0)) };
411            Box::new(ScrollLinked::new(row, step_link(i, 0.35))) as AnyView
412        })
413        .collect();
414    let panes: Vec<AnyView> = (0..STEPS.len())
415        .map(|i| {
416            let pane = stack((
417                stack((step_visual(i, &clicks),)).align(Align::Center).frame(None, Some(240.0)),
418                CodeBlock::new(CODE[i]),
419            ))
420            .spacing(16.0)
421            .align(Align::Stretch);
422            Box::new(ScrollLinked::new(pane, step_link(i, 0.0))) as AnyView
423        })
424        .collect();
425    let text = stack((
426        kicker("From state to pixels"),
427        heading("One frame, six steps, no webview."),
428        lede("Scroll: the section holds while a click travels through the engine."),
429        stack(steps).spacing(14.0),
430    ))
431    .spacing(14.0);
432    let panel = Card::new(ZStack::new(panes)).padding(20.0);
433    let body: AnyView = if narrow {
434        Box::new(stack((panel, text)).spacing(20.0))
435    } else {
436        Box::new(HStack::new((text.grow(1.0), panel.grow(1.0))).spacing(48.0).align(Align::Start))
437    };
438    ScrollScene::new(stack((column(body),)).padding(Insets::symmetric(72.0, 0.0)))
439        .length(if narrow { 1800.0 } else { 2400.0 })
440        .top(BAR)
441}
442
443fn tile<V: View + 'static>(name: &str, content: V) -> impl View {
444    Card::new(
445        stack((
446            content,
447            Text::new(name.to_uppercase()).style(TextStyle::Caption).foreground(Semantic::InkMuted),
448        ))
449        .spacing(14.0),
450    )
451    .padding(18.0)
452    .frame(Some(300.0), None)
453}
454
455fn components(narrow: bool) -> impl View {
456    let (on, off, checked) = (State::new(true), State::new(false), State::new(true));
457    let (email, level, segment) =
458        (State::new(String::from("you@example.com")), State::new(0.62_f32), State::new(0));
459    let first = HStack::new((
460        tile(
461            "Button",
462            HStack::new((Button::new("Save changes"), Button::new("Cancel").style(ButtonStyle::Secondary)))
463                .spacing(8.0),
464        ),
465        tile(
466            "Toggle",
467            stack((
468                Toggle::new(on.binding()).label("Sync on Wi-Fi"),
469                Toggle::new(off.binding()).label("Weekly digest"),
470            ))
471            .spacing(10.0),
472        ),
473        tile(
474            "Badge",
475            HStack::new((
476                Badge::new("Overdue").tone(Tone::Danger),
477                Badge::new("Due today").tone(Tone::Warning),
478                Badge::new("Paid").tone(Tone::Success),
479            ))
480            .spacing(6.0),
481        ),
482        tile("TextField", TextField::new(email.binding()).icon(IconName::Mail)),
483        tile("Stat", Stat::new("Active users", "8,214").delta("+3.1%", true)),
484        tile(
485            "Avatar",
486            HStack::new((Avatar::new("Ana Lima"), Avatar::new("Bruno Reis"), Avatar::new("Carla Dias")))
487                .spacing(6.0),
488        ),
489    ))
490    .spacing(16.0);
491    let second = HStack::new((
492        tile("SegmentedControl", SegmentedControl::new(segment.binding(), ["Day", "Week", "Month"])),
493        tile("Checkbox", Checkbox::new("Email me receipts", checked.binding())),
494        tile("Slider", Slider::new(level.binding()).label("Volume")),
495        tile("ProgressBar", ProgressBar::new(0.7).label("Exporting PDF").percent()),
496        tile(
497            "Kbd",
498            HStack::new((Kbd::new("⌘"), Kbd::new("K"), Text::new("Search").foreground(Semantic::InkMuted)))
499                .spacing(6.0),
500        ),
501        tile(
502            "BarChart",
503            BarChart::new([("Mon", 42.0), ("Tue", 58.0), ("Wed", 51.0), ("Thu", 73.0)]).height(90.0),
504        ),
505    ))
506    .spacing(16.0);
507    let stat = |label: &str, value: f64, suffix: &'static str, note: &str| {
508        Card::new(
509            stack((
510                Text::new(label.to_uppercase()).style(TextStyle::Caption).foreground(Semantic::InkMuted),
511                CountUp::new(value).format(move |v| format!("{}{suffix}", group(v))),
512                Badge::new(note),
513            ))
514            .spacing(8.0),
515        )
516        .padding(24.0)
517    };
518    stack((
519        section(stack((
520            kicker("Components"),
521            heading("160+ components, already designed and tested."),
522            lede("Every component reads the same tokens, works with the keyboard and screen readers, and runs unchanged on desktop and the web. These are live: try them."),
523        ))
524        .spacing(14.0)),
525        Ticker::new(first).speed(30.0),
526        Ticker::new(second).speed(34.0).reverse(),
527        section(Grid::new(
528            if narrow { 2 } else { 4 },
529            (
530                stat("Components", 160.0, "+", "One token set"),
531                stat("DataGrid rows", 100_000.0, "", "Virtualized"),
532                stat("Report pages", 305.0, "", "In about 0.2 s"),
533                stat("Platforms", 4.0, "", "One renderer"),
534            ),
535        )
536        .spacing(16.0)),
537    ))
538    .spacing(16.0)
539}
540
541/// `1234567.0` as `1,234,567`.
542fn group(v: f64) -> String {
543    let digits = (v.round() as u64).to_string();
544    let mut out = String::new();
545    for (i, c) in digits.chars().enumerate() {
546        if i > 0 && (digits.len() - i).is_multiple_of(3) {
547            out.push(',');
548        }
549        out.push(c);
550    }
551    out
552}
553
554fn reporting(reports: Rc<Reports>, narrow: bool, links: Links) -> impl View {
555    let last = reports.sales.pages.len().saturating_sub(1);
556    let fan = PageFan::new()
557        .width(if narrow { 200.0 } else { 260.0 })
558        .spread(if narrow { 20.0 } else { 56.0 })
559        .page(&reports.customers, 0, "Customer list", DESIGNER)
560        .page(&reports.invoice, 0, "Invoice", DESIGNER)
561        .page(&reports.dashboard, 0, "Sales dashboard", DESIGNER)
562        .page(&reports.sales, last, "Sales by region, last page", DESIGNER)
563        .page(&reports.sales, 1, "Sales by region, page 2", DESIGNER)
564        .page(&reports.sales, 0, "Sales by region, page 1", DESIGNER);
565    let words = stack((
566        kicker("RavenRS Reporting"),
567        heading("From data to a finished report in under a minute."),
568        lede(
569            "A report designer your users run in the browser, and an engine that lays out 10,000 rows in 305 pages \
570             in about 0.2 seconds. These pages are its real output, drawn by RavenRS as you look.",
571        ),
572        HStack::new((
573            Button::new("Open the report designer").on_click(go(&links, DESIGNER)),
574            Button::new("Feature matrix")
575                .style(ButtonStyle::Secondary)
576                .on_click(go(&links, "https://github.com/alexkads/RavenRS/blob/master/docs/REPORTING-PARITY.md")),
577        ))
578        .spacing(12.0)
579        .wrap(),
580    ))
581    .spacing(14.0);
582    let feature = |title: &str, text: &str| {
583        Card::new(stack((Text::new(title).style(TextStyle::Headline), lede(text))).spacing(6.0)).padding(20.0)
584    };
585    let top: AnyView = if narrow {
586        Box::new(stack((words, stack((fan,)).align(Align::Center))).spacing(32.0))
587    } else {
588        Box::new(HStack::new((words.grow(1.0), fan)).spacing(40.0).align(Align::Center))
589    };
590    section(
591        stack((
592            top,
593            Grid::new(
594                if narrow { 1 } else { 4 },
595                (
596                    feature(
597                        "Bands and groups",
598                        "Headers and footers, nested groups with subtotals, master-detail.",
599                    ),
600                    feature("Expressions", "Sum([Total]), Iif(...), calculated fields and parameters."),
601                    feature("Reports are data", "One JSON file holds the layout and its data sources."),
602                    feature("Same engine everywhere", "Desktop, browser and server produce the same pages."),
603                ),
604            )
605            .spacing(16.0),
606        ))
607        .spacing(40.0),
608    )
609}
610
611const PLATFORMS: [&str; 4] = ["macOS", "Windows", "Linux", "Web"];
612
613fn platforms(narrow: bool, platform: State<usize>) -> impl View {
614    let chosen = platform.clone();
615    let title_bar = view_fn(move || -> AnyView {
616        let name = PLATFORMS[chosen.get().min(3)];
617        let title = Text::new(format!("Finance — RavenRS on {name}"))
618            .style(TextStyle::Callout)
619            .foreground(Semantic::InkMuted);
620        Box::new(
621            HStack::new((
622                if chosen.get() == 0 {
623                    Box::new(
624                        HStack::new((
625                            Dot::new().tone(Tone::Danger),
626                            Dot::new().tone(Tone::Warning),
627                            Dot::new().tone(Tone::Success),
628                        ))
629                        .spacing(6.0),
630                    ) as AnyView
631                } else {
632                    Box::new(Icon::new(IconName::Layers).color(Semantic::Accent)) as AnyView
633                },
634                Spacer::new(),
635                title,
636                Spacer::new(),
637            ))
638            .spacing(8.0)
639            .align(Align::Center)
640            .padding(12.0),
641        )
642    });
643    let sidebar = Sidebar::new((SidebarGroup::new(
644        "Day to day",
645        (
646            SidebarItem::new(IconName::LayoutDashboard, "Overview").selected(true),
647            SidebarItem::new(IconName::CreditCard, "Accounts").badge(Badge::new("2").tone(Tone::Danger)),
648            SidebarItem::new(IconName::Target, "Budgets"),
649            SidebarItem::new(IconName::FileText, "Statements"),
650        ),
651    ),));
652    let main = stack((
653        Text::new("Overview").style(TextStyle::Display),
654        Grid::new(
655            2,
656            (
657                Card::new(Stat::new("Balance", "R$ 18.240").delta("+4.2%", true)).padding(20.0),
658                Card::new(Stat::new("Spent this month", "R$ 4.917").delta("-0.4%", false)).padding(20.0),
659            ),
660        )
661        .spacing(16.0),
662        Card::new(
663            BarChart::new([
664                ("Mon", 48.0),
665                ("Tue", 70.0),
666                ("Wed", 62.0),
667                ("Thu", 96.0),
668                ("Fri", 86.0),
669                ("Sat", 34.0),
670            ])
671            .height(150.0),
672        )
673        .padding(20.0),
674    ))
675    .spacing(16.0)
676    .padding(24.0)
677    .grow(1.0);
678    let window_body: AnyView =
679        if narrow { Box::new(main) } else { Box::new(HStack::new((sidebar, main)).align(Align::Stretch)) };
680    section(
681        stack((
682            kicker("Runs everywhere"),
683            heading("The same pixels on every platform, in three themes."),
684            lede("Switch the platform, then the theme: the whole page redraws in it, because it is a RavenRS app."),
685            HStack::new((SegmentedControl::new(platform.binding(), PLATFORMS), Spacer::new(), ThemeSwitcher::new()))
686                .spacing(12.0)
687                .wrap(),
688            Card::new(stack((title_bar, Divider::horizontal(), window_body)).align(Align::Stretch)),
689        ))
690        .spacing(16.0),
691    )
692}
693
694fn roadmap(narrow: bool) -> impl View {
695    let phase = |n: u8, title: &str, text: &str, state: (&str, Tone)| {
696        let row = HStack::new((
697            Text::new(format!("PHASE {n}"))
698                .style(TextStyle::Caption)
699                .foreground(Semantic::InkMuted)
700                .frame(Some(90.0), None),
701            stack((Text::new(title).style(TextStyle::Headline), lede(text))).spacing(2.0).grow(1.0),
702            Badge::new(state.0).tone(state.1),
703        ))
704        .spacing(16.0)
705        .align(Align::Center);
706        stack((row, Divider::horizontal())).spacing(14.0).align(Align::Stretch)
707    };
708    let done = ("Done", Tone::Success);
709    let next = ("In progress", Tone::Accent);
710    let _ = narrow;
711    section(
712        stack((
713            kicker("Roadmap"),
714            heading("Built in the open, one phase at a time."),
715            Card::new(
716                stack((
717                    ProgressBar::new(3.0 / 7.0).label("3 of 7 phases done"),
718                    phase(
719                        1,
720                        "Foundation",
721                        "Workspace, checks, wgpu on desktop and the web, SDF shapes, text.",
722                        done,
723                    ),
724                    phase(
725                        2,
726                        "Components",
727                        "160+ components, a Ribbon, a scheduler, docking and a 100k-row DataGrid.",
728                        done,
729                    ),
730                    phase(
731                        3,
732                        "Quality and release",
733                        "crates.io, WAI-ARIA keyboard patterns, golden images, budgets.",
734                        done,
735                    ),
736                    phase(
737                        4,
738                        "Accessibility and platform",
739                        "Screen readers, clipboard and IME on the web are done.",
740                        next,
741                    ),
742                    phase(
743                        5,
744                        "Reporting",
745                        "Engine, web designer, PDF, XLSX and CSV export, scheduled delivery.",
746                        next,
747                    ),
748                    phase(
749                        6,
750                        "Polish",
751                        "Vector chart lines, Glass backdrop blur, rebuilding only what changed.",
752                        ("Planned", Tone::Neutral),
753                    ),
754                    phase(
755                        7,
756                        "Applications",
757                        "Business apps from a model, local AI, an MCP server, training films.",
758                        next,
759                    ),
760                ))
761                .spacing(14.0)
762                .align(Align::Stretch),
763            )
764            .padding(24.0),
765        ))
766        .spacing(16.0),
767    )
768}
769
770fn closing(links: Links) -> impl View {
771    stack((
772        section(
773            stack((
774                Text::new("Build the interface you would be proud to ship.").style(TextStyle::Display),
775                lede("Open source under MIT or Apache-2.0. Issues, ideas and pull requests are welcome."),
776                HStack::new((
777                    Button::new("Read the getting started guide")
778                        .size(ButtonSize::Large)
779                        .on_click(go(&links, GUIDE)),
780                    Button::new("Browse the API docs")
781                        .style(ButtonStyle::Secondary)
782                        .size(ButtonSize::Large)
783                        .on_click(go(&links, API)),
784                ))
785                .spacing(12.0)
786                .wrap(),
787            ))
788            .spacing(18.0)
789            .align(Align::Center),
790        ),
791        Divider::horizontal(),
792        column(
793            HStack::new((
794                lede("RavenRS · MIT OR Apache-2.0"),
795                Spacer::new(),
796                lede("Drawn by RavenRS with WebGL2. Set in Inter."),
797            ))
798            .padding(Insets::symmetric(24.0, 0.0)),
799        ),
800    ))
801    .align(Align::Stretch)
802}
803
804fn command_palette(open: State<bool>, links: Links) -> impl View {
805    CommandPalette::new(open.binding())
806        .command(Command::new("Getting started", go(&links, GUIDE)).icon(IconName::BookOpen).group("Guides"))
807        .command(Command::new("Component gallery", go(&links, DEMO)).icon(IconName::Play).group("Try it"))
808        .command(Command::new("Playground", go(&links, PLAYGROUND)).icon(IconName::Code).group("Try it"))
809        .command(
810            Command::new("Report designer", go(&links, DESIGNER)).icon(IconName::FileText).group("Try it"),
811        )
812        .command(
813            Command::new("All components", go(&links, COMPONENTS))
814                .icon(IconName::LayoutDashboard)
815                .group("Reference"),
816        )
817        .command(Command::new("API docs", go(&links, API)).icon(IconName::Code).group("Reference"))
818        .command(
819            Command::new("Reports on this page", go(&links, "#reports"))
820                .icon(IconName::FileText)
821                .group("This page"),
822        )
823        .command(Command::new("Roadmap", go(&links, "#roadmap")).icon(IconName::Target).group("This page"))
824        .command(Command::new("GitHub repository", go(&links, GITHUB)).icon(IconName::Star).group("Source"))
825}
826
827#[cfg(target_arch = "wasm32")]
828mod web_entry {
829    use std::cell::RefCell;
830
831    use wasm_bindgen::prelude::*;
832
833    thread_local! {
834        static APP: RefCell<Option<ravenrs::web::WebApp>> = const { RefCell::new(None) };
835    }
836
837    #[wasm_bindgen(start)]
838    pub fn start() {
839        console_error_panic_hook::set_once();
840        wasm_bindgen_futures::spawn_local(async {
841            // WebGL2 opens in every browser and phone, with or without WebGPU.
842            let options =
843                ravenrs::web::WebOptions::new("raven-canvas").backend(ravenrs::web::WebBackend::WebGl2);
844            match ravenrs::web::start(super::Home::new(), options).await {
845                Ok(app) => APP.with(|a| *a.borrow_mut() = Some(app)),
846                Err(e) => web_sys_log(&e.to_string()),
847            }
848        });
849    }
850
851    fn web_sys_log(message: &str) {
852        let _ = message;
853    }
854}