Skip to main content

wisp_storybook/stories/
s_editor_background.rs

1//! Story: editor background framing (ED.18 / M-EDIT).
2//!
3//! The cinematic "framed screen" look: the recording sits on a colored
4//! backdrop, lifted off it by a uniform padding and clipped to a
5//! rounded-corner window. This mirrors the editor's real compose — a
6//! full-NDC gradient `Graphics` backdrop drawn behind a rounded-rect-clipped
7//! "screen" — exactly the structure `EditorPreview::set_background` builds in
8//! `wisp::RecordingScene` (backdrop Phase 1, clipped screen Phase 2).
9//!
10//! Single-bind-group clip + graphics (no blur), so it runs on every CI OS —
11//! NOT in `LAVAPIPE_INCOMPATIBLE`.
12
13use wisp::application::Application;
14use wisp::math::Rect;
15use wisp::{Color, Container, Fill, Graphics, MaskShape, Node, Stage, Stroke};
16
17pub fn story() -> crate::story::Story {
18    crate::story::Story {
19        id: "editor-background-framing",
20        category: "Editor",
21        title: "Background framing — backdrop · padding · rounded corners",
22        milestone: "ED.18",
23        writeup: include_str!("writeups/editor_background.md"),
24        build,
25        tick: None,
26    }
27}
28
29fn build(_app: &Application, stage: &mut Stage) {
30    // Backdrop: the default "Aurora" warm→cool diagonal gradient, filling the
31    // whole canvas. In the real compose this is the non-clipped Phase-1 node.
32    let mut backdrop = Graphics::new();
33    let theta = 135.0_f32.to_radians();
34    let dir = glam::Vec2::new(theta.cos(), theta.sin());
35    backdrop.fill(Fill::LinearGradient {
36        start: -dir,
37        end: dir,
38        color_a: Color::rgb_u8(255, 138, 128),
39        color_b: Color::rgb_u8(40, 53, 147),
40    });
41    backdrop.draw_rect(Rect::new(-1.0, -1.0, 2.0, 2.0));
42    let _ = stage.add_child(stage.root(), backdrop);
43
44    // Drop shadow (ED.18): a dark rounded-rect the shape of the frame window,
45    // offset down-right, drawn behind the screen so the offset sliver reads as
46    // a shadow lifting the screen off the backdrop. Phase-1 (unclipped),
47    // composited after the backdrop and under the screen.
48    let mut shadow = Graphics::new();
49    shadow.fill(Fill::Solid(Color::rgba(0.0, 0.0, 0.0, 0.45)));
50    shadow.draw_rounded_rect(Rect::new(-0.84 + 0.03, -0.78 - 0.05, 1.68, 1.56), 0.06);
51    let _ = stage.add_child(stage.root(), shadow);
52
53    // The framed screen: a rounded-rect window, inset by the padding. In the
54    // real compose this is the clipped screen sprite (dispatched Phase 2, so
55    // it composites over the backdrop). Defaults (padding 64, corner 14 on a
56    // 1920-wide canvas) → window ≈ [-0.93, 0.93] × [-0.88, 0.88], radius
57    // ≈ 0.015; widened a touch here so the inset reads at thumbnail size.
58    let mut window = Container::new();
59    window.clip = Some(MaskShape::rounded_rect(
60        Rect::new(-0.84, -0.78, 1.68, 1.56),
61        0.06,
62    ));
63    let window_id = stage
64        .add_child(stage.root(), Node::Container(window))
65        .expect("window container");
66
67    // Light "screen" content so the colored backdrop margin + rounded corners
68    // read clearly (the storybook backdrop-visibility convention: light
69    // content over a colored frame). A pale vertical gradient with a single
70    // accent bar stands in for a screenshot.
71    let mut screen = Graphics::new();
72    screen.fill(Fill::LinearGradient {
73        start: glam::Vec2::new(0.0, 1.0),
74        end: glam::Vec2::new(0.0, -1.0),
75        color_a: Color::rgb_u8(248, 250, 252),
76        color_b: Color::rgb_u8(214, 222, 232),
77    });
78    screen.draw_rect(Rect::new(-1.0, -1.0, 2.0, 2.0));
79    // Accent "title bar" so the screen reads as content, not a flat panel.
80    screen.fill(Fill::Solid(Color::rgb_u8(60, 110, 220)));
81    screen.draw_rect(Rect::new(-0.84, 0.58, 1.68, 0.2));
82    let _ = stage.add_child(window_id, screen);
83
84    // Inset border (ED.18): a rounded-rect stroke tracing the frame window,
85    // over the screen. A full-NDC clip forces Phase-2 dispatch so it composites
86    // on top (mirrors RecordingScene::set_frame_border).
87    let mut border = Graphics::new();
88    border.container.clip = Some(MaskShape::rect(Rect::new(-1.0, -1.0, 2.0, 2.0)));
89    border.fill(Fill::Solid(Color::rgba(0.0, 0.0, 0.0, 0.0)));
90    border.stroke(Some(Stroke::new(0.012, Color::rgba(1.0, 1.0, 1.0, 0.85))));
91    border.draw_rounded_rect(Rect::new(-0.84, -0.78, 1.68, 1.56), 0.06);
92    let _ = stage.add_child(stage.root(), border);
93}