1use std::rc::Rc;
7
8use ravenrs::core::Key;
9use ravenrs::prelude::*;
10use ravenrs_report::{Document, ReportEngine, samples};
11use ravenrs_report_viewer::PageFan;
12
13const 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/";
21const BAR: f32 = 64.0;
23
24#[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#[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 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 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
108fn stack<C: ViewSeq>(children: C) -> VStack<C> {
110 VStack::new(children).align(Align::Stretch)
111}
112
113fn go(links: &Links, url: &'static str) -> impl Fn() + 'static {
115 let links = links.clone();
116 move || links.open(url)
117}
118
119fn 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
182fn 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
330fn 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 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
541fn 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 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}