1use ravenrs_core::{Insets, Role};
4use ravenrs_layout::{Align, Axis, LayoutStyle};
5use ravenrs_reactive::Binding;
6use ravenrs_style::{Semantic, TextStyle, Weight};
7
8use crate::context::{BuildCx, NodeId, Paint, Semantics};
9use crate::{Dialog, Kbd, View, util};
10
11const SHEET_GROUP_WIDTH: f32 = 280.0;
13const SHEET_HEIGHT_SHARE: f32 = 0.7;
15const SHEET_LINE: f32 = 30.0;
17
18#[derive(Clone, Debug)]
20struct ShortcutGroup {
21 title: String,
22 rows: Vec<(String, String)>,
23}
24
25#[derive(Debug)]
40#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
41pub struct ShortcutSheet {
42 open: Binding<bool>,
43 title: Option<String>,
44 groups: Vec<ShortcutGroup>,
45 width: f32,
46}
47
48#[cfg_attr(feature = "live", ravenrs_live::expose)]
49impl ShortcutSheet {
50 pub fn new(open: Binding<bool>) -> Self {
52 Self { open, title: None, groups: Vec::new(), width: 2.0 * SHEET_GROUP_WIDTH + 100.0 }
53 }
54
55 pub fn title(mut self, title: impl Into<String>) -> Self {
57 self.title = Some(title.into());
58 self
59 }
60
61 pub fn group<K: Into<String>, D: Into<String>>(
63 mut self,
64 title: impl Into<String>,
65 rows: impl IntoIterator<Item = (K, D)>,
66 ) -> Self {
67 let rows = rows.into_iter().map(|(keys, does)| (keys.into(), does.into())).collect();
68 self.groups.push(ShortcutGroup { title: title.into(), rows });
69 self
70 }
71
72 pub fn width(mut self, width: f32) -> Self {
74 self.width = width;
75 self
76 }
77}
78
79struct SheetBody {
81 groups: Vec<ShortcutGroup>,
82}
83
84#[cfg_attr(feature = "live", ravenrs_live::expose)]
85impl SheetBody {
86 fn row(cx: &mut BuildCx<'_>, keys: &str, does: &str) -> NodeId {
87 let theme = *cx.theme();
88 let cap = cx.child(&Kbd::new(keys));
89 let cap = cx.container(
91 LayoutStyle { min_width: Some(64.0), ..LayoutStyle::stack(Axis::Horizontal) },
92 &[cap],
93 Paint::None,
94 );
95 let does = util::label(cx, does, TextStyle::Callout, None, theme.color(Semantic::InkMuted));
96 cx.container(
97 LayoutStyle {
98 spacing: theme.spacing.s2,
99 align: Align::Center,
100 min_height: Some(SHEET_LINE - theme.spacing.s1),
101 ..LayoutStyle::stack(Axis::Horizontal)
102 },
103 &[cap, does],
104 Paint::None,
105 )
106 }
107}
108
109impl View for SheetBody {
110 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
111 let theme = *cx.theme();
112 let mut groups = Vec::with_capacity(self.groups.len());
113 for group in &self.groups {
114 let title = util::label(
115 cx,
116 &group.title,
117 TextStyle::Caption,
118 Some(Weight::SEMIBOLD),
119 theme.color(Semantic::InkMuted),
120 );
121 cx.semantics(title, Semantics::new(Role::Heading).level(3));
122 let mut column = vec![title];
123 for (keys, does) in &group.rows {
124 column.push(Self::row(cx, keys, does));
125 }
126 groups.push(cx.container(
127 LayoutStyle {
128 width: Some(SHEET_GROUP_WIDTH),
129 spacing: theme.spacing.s1,
130 ..LayoutStyle::stack(Axis::Vertical)
131 },
132 &column,
133 Paint::None,
134 ));
135 }
136 let lines: usize = self.groups.iter().map(|g| g.rows.len() + 1).sum();
137 let room = cx.viewport().height * SHEET_HEIGHT_SHARE;
138 let scrolls = lines as f32 * SHEET_LINE > room;
141 let node = cx.container(
142 LayoutStyle {
143 spacing: theme.spacing.s5,
144 wrap: true,
145 height: scrolls.then_some(room),
146 overflow_hidden: scrolls,
147 padding: Insets::ZERO,
148 ..LayoutStyle::stack(Axis::Horizontal)
149 },
150 &groups,
151 Paint::None,
152 );
153 if scrolls {
154 cx.scrollable(node);
155 cx.semantics(node, Semantics::new(Role::ScrollView));
156 }
157 node
158 }
159}
160
161impl View for ShortcutSheet {
162 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
163 let title = self.title.clone().unwrap_or_else(|| cx.labels().shortcut_sheet.clone());
164 let groups = if self.open.get() { self.groups.clone() } else { Vec::new() };
166 let dialog = Dialog::new(self.open.clone(), title).width(self.width).content(SheetBody { groups });
167 cx.child(&dialog)
168 }
169}
170
171#[derive(Clone, Debug, Default)]
182#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
183pub struct ShortcutHintBar {
184 leading: Option<String>,
185 hints: Vec<(Vec<String>, String)>,
186}
187
188#[cfg_attr(feature = "live", ravenrs_live::expose)]
189impl ShortcutHintBar {
190 pub fn new() -> Self {
192 Self::default()
193 }
194
195 pub fn leading(mut self, text: impl Into<String>) -> Self {
197 self.leading = Some(text.into());
198 self
199 }
200
201 pub fn hint<K: Into<String>>(
203 mut self,
204 keys: impl IntoIterator<Item = K>,
205 does: impl Into<String>,
206 ) -> Self {
207 self.hints.push((keys.into_iter().map(Into::into).collect(), does.into()));
208 self
209 }
210}
211
212impl View for ShortcutHintBar {
213 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
214 let theme = *cx.theme();
215 let muted = theme.color(Semantic::InkMuted);
216 let mut children = Vec::with_capacity(self.hints.len() + 1);
217 if let Some(text) = &self.leading {
218 children.push(util::label(cx, text, TextStyle::Caption, None, muted));
219 }
220 for (keys, does) in &self.hints {
221 let mut hint: Vec<NodeId> = keys.iter().map(|key| cx.child(&Kbd::new(key.clone()))).collect();
222 hint.push(util::label(cx, does, TextStyle::Caption, None, muted));
223 children.push(cx.container(
224 LayoutStyle {
225 spacing: theme.spacing.s1,
226 align: Align::Center,
227 ..LayoutStyle::stack(Axis::Horizontal)
228 },
229 &hint,
230 Paint::None,
231 ));
232 }
233 cx.container(
234 LayoutStyle {
235 spacing: theme.spacing.s3,
236 align: Align::Center,
237 wrap: true,
238 ..LayoutStyle::stack(Axis::Horizontal)
239 },
240 &children,
241 Paint::None,
242 )
243 }
244}