1use std::cell::RefCell;
4use std::rc::Rc;
5
6use ravenrs_core::{Color, CursorIcon, Effect, Point, Quad, Rect, Role, Scene, Shadow};
7use ravenrs_layout::{Axis, LayoutStyle};
8use ravenrs_report::{Document, Page, paint_page};
9use ravenrs_style::Semantic;
10use ravenrs_text::TextSystem;
11use ravenrs_widgets::{
12 Badge, BuildCx, Motion, NodeId, Paint, ScrollLink, ScrollLinked, ScrollRange, Semantics, View,
13};
14
15#[derive(Clone, Debug)]
17struct FanPage {
18 page: Rc<Page>,
19 label: String,
20 url: String,
21}
22
23#[derive(Clone, Debug)]
37pub struct PageFan {
38 pages: Vec<FanPage>,
39 width: f32,
40 spread: f32,
41 hint: String,
42}
43
44impl Default for PageFan {
45 fn default() -> Self {
46 Self::new()
47 }
48}
49
50impl PageFan {
51 pub fn new() -> Self {
53 Self { pages: Vec::new(), width: 260.0, spread: 62.0, hint: "Open in the designer".to_owned() }
54 }
55
56 pub fn page(
59 mut self,
60 document: &Document,
61 index: usize,
62 label: impl Into<String>,
63 url: impl Into<String>,
64 ) -> Self {
65 if let Some(page) = document.pages.get(index) {
66 self.pages.push(FanPage { page: Rc::new(page.clone()), label: label.into(), url: url.into() });
67 }
68 self
69 }
70
71 pub fn width(mut self, width: f32) -> Self {
73 self.width = width.max(40.0);
74 self
75 }
76
77 pub fn spread(mut self, spread: f32) -> Self {
79 self.spread = spread.max(0.0);
80 self
81 }
82
83 pub fn hint(mut self, hint: impl Into<String>) -> Self {
85 self.hint = hint.into();
86 self
87 }
88
89 fn height(&self) -> f32 {
90 self.pages.iter().map(|p| p.page.height / p.page.width.max(1.0) * self.width).fold(0.0, f32::max)
91 }
92}
93
94impl View for PageFan {
95 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
96 let count = self.pages.len();
97 let front = cx.use_state(|| None::<(usize, bool)>);
99 let lifted = match front.get() {
100 Some((i, true)) => Some(i),
101 Some((i, false)) if cx.is_hover_within() => Some(i),
103 _ => None,
104 };
105 let cache = cx.use_state(|| Rc::new(RefCell::new(Pictures::default()))).get();
106 let (width, height) = (self.width, self.height());
107 let span = self.spread * count.saturating_sub(1) as f32;
108 let total = Rect::new(0.0, 0.0, width + span, height + 64.0);
109 let mut order: Vec<usize> = (0..count).filter(|i| Some(*i) != lifted).collect();
110 order.extend(lifted);
111 let mut children = Vec::with_capacity(count);
112 for i in order {
113 let page = &self.pages[i];
114 let off = i as f32 - (count as f32 - 1.0) / 2.0;
115 let card = Card {
116 page: page.clone(),
117 index: i,
118 width,
119 height: page.page.height / page.page.width.max(1.0) * width,
120 lifted: lifted == Some(i),
121 dimmed: lifted.is_some_and(|l| l != i),
122 front: front.clone(),
123 hint: self.hint.clone(),
124 cache: cache.clone(),
125 };
126 let closed = Effect::moved(-off * self.spread, -3.0 * i as f32);
128 let link =
129 ScrollLink::keyframes([(0.0, closed), (1.0, Effect::IDENTITY)]).range(ScrollRange::Enter);
130 let placed = Placed {
131 x: span / 2.0 + off * self.spread,
132 y: 24.0 + (off.abs() * 6.0),
133 content: ScrollLinked::new(card, link),
134 };
135 children.push(cx.keyed_child(i as u64, &placed));
136 }
137 cx.container(
138 LayoutStyle {
139 width: Some(total.width()),
140 height: Some(total.height()),
141 ..LayoutStyle::stack(Axis::Vertical)
142 },
143 &children,
144 Paint::None,
145 )
146 }
147}
148
149struct Placed<C> {
151 x: f32,
152 y: f32,
153 content: C,
154}
155
156impl<C: View> View for Placed<C> {
157 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
158 let child = cx.child(&self.content);
159 cx.container(
160 LayoutStyle { absolute: Some((self.x, self.y)), ..LayoutStyle::stack(Axis::Vertical) },
161 &[child],
162 Paint::None,
163 )
164 }
165}
166
167#[derive(Default)]
169struct Pictures {
170 text: Option<TextSystem>,
171 scenes: Vec<(usize, u32, Rc<Scene>)>,
172}
173
174impl Pictures {
175 fn get(&mut self, index: usize, page: &Page, width: f32) -> Rc<Scene> {
176 let key = width.to_bits();
177 if let Some((_, _, scene)) = self.scenes.iter().find(|(i, w, _)| *i == index && *w == key) {
178 return scene.clone();
179 }
180 let mut scene = Scene::new();
181 paint_page(
182 page,
183 self.text.get_or_insert_with(TextSystem::new),
184 &mut scene,
185 Point::ZERO,
186 width / page.width.max(1.0),
187 );
188 let scene = Rc::new(scene);
189 self.scenes.push((index, key, scene.clone()));
190 scene
191 }
192}
193
194struct Card {
195 page: FanPage,
196 index: usize,
197 width: f32,
198 height: f32,
199 lifted: bool,
200 dimmed: bool,
201 front: ravenrs_reactive::State<Option<(usize, bool)>>,
202 hint: String,
203 cache: Rc<RefCell<Pictures>>,
204}
205
206impl View for Card {
207 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
208 let theme = *cx.theme();
209 let picture = self.cache.borrow_mut().get(self.index, &self.page.page, self.width);
210 let (lifted, dimmed) = (self.lifted, self.dimmed);
211 let ring = theme.color(Semantic::Accent);
212 let paint = Paint::Custom(Rc::new(move |frame: Rect, scene: &mut Scene| {
213 let start = scene.len();
214 scene.extend_from_slice(picture.primitives());
215 scene.apply_effect(start..scene.len(), Effect::moved(frame.x(), frame.y()), Point::ZERO);
216 if dimmed {
217 scene.push_quad(Quad::new(frame, Color::rgba(0.04, 0.05, 0.07, 0.38)));
218 }
219 if lifted {
220 let shadowed = Quad::new(frame, Color::TRANSPARENT).border(2.0, ring).shadow(Shadow {
221 offset: Point::new(0.0, 16.0),
222 blur: 36.0,
223 color: Color::rgba(0.0, 0.0, 0.0, 0.45),
224 });
225 scene.push_quad(shadowed);
226 }
227 }));
228 let page = cx.container(
229 LayoutStyle {
230 width: Some(self.width),
231 height: Some(self.height),
232 ..LayoutStyle::stack(Axis::Vertical)
233 },
234 &[],
235 paint,
236 );
237 let mut parts = vec![page];
238 if lifted {
239 let label = format!("{} · {}", self.page.label, self.hint);
240 parts.push(cx.child(&Badge::new(label)));
241 }
242 let node = cx.container(
243 LayoutStyle {
244 spacing: 8.0,
245 align: ravenrs_layout::Align::Center,
246 ..LayoutStyle::stack(Axis::Vertical)
247 },
248 &parts,
249 Paint::None,
250 );
251 let lift = if lifted { Effect::moved(0.0, -22.0).with_scale(1.06) } else { Effect::IDENTITY };
252 cx.motion(node, Motion::new().effect(lift, theme.motion.snappy));
253 let (links, url) = (cx.links(), self.page.url.clone());
254 cx.on_click(node, Rc::new(move || links.open(&url)));
255 let (front, index) = (self.front.clone(), self.index);
256 cx.on_pointer_move(
257 node,
258 Rc::new(move |_, _| {
259 if front.get() != Some((index, false)) {
260 front.set(Some((index, false)));
261 }
262 }),
263 );
264 let by_focus = Some((self.index, true));
265 if cx.is_focused() && self.front.get() != by_focus {
266 self.front.set(by_focus);
267 } else if !cx.is_focused() && self.front.get() == by_focus {
268 self.front.set(None);
269 }
270 cx.semantics(node, Semantics::new(Role::Link).name(format!("{}. {}", self.page.label, self.hint)));
271 cx.focusable(node, 10.0);
272 cx.cursor(node, CursorIcon::Pointer);
273 node
274 }
275}