1use std::fmt;
6
7use ravenrs_criteria::{Category, Criteria, FieldType, FunctionInfo, Problem, Schema, functions, validate};
8use ravenrs_layout::Align;
9use ravenrs_reactive::{Binding, State};
10use ravenrs_style::{SPACING, Semantic, TextStyle, Weight};
11
12use crate::context::{BuildCx, NodeId};
13use crate::text_field::{Cursor, byte_index};
14use crate::{
15 Alert, AnyView, Button, ButtonSize, FilterField, FilterFieldKind, HStack, IconName, ScrollView,
16 SidebarItem, Text, TextArea, TextField, Tone, VStack, View, ViewExt,
17};
18
19const FUNCTION_GROUPS: [Category; 6] = [
22 Category::Logical,
23 Category::String,
24 Category::Math,
25 Category::DateTime,
26 Category::Reporting,
27 Category::Aggregate,
28];
29const OPERATORS: usize = 8;
30const CONSTANTS: usize = 9;
31
32const LISTS_HEIGHT: f32 = 340.0;
34const GROUPS_WIDTH: f32 = 140.0;
35const ITEMS_WIDTH: f32 = 200.0;
36
37#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
59pub struct ExpressionEditor {
60 value: Binding<String>,
61 fields: Vec<FilterField>,
62 parameters: Vec<FilterField>,
63 rows: usize,
64}
65
66impl fmt::Debug for ExpressionEditor {
67 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68 f.debug_struct("ExpressionEditor")
69 .field("fields", &self.fields.len())
70 .field("parameters", &self.parameters.len())
71 .finish_non_exhaustive()
72 }
73}
74
75#[cfg_attr(feature = "live", ravenrs_live::expose)]
76impl ExpressionEditor {
77 pub fn new(value: Binding<String>) -> Self {
79 Self { value, fields: Vec::new(), parameters: Vec::new(), rows: 5 }
80 }
81
82 pub fn field(mut self, field: FilterField) -> Self {
84 self.fields.push(field);
85 self
86 }
87
88 pub fn fields(mut self, fields: impl IntoIterator<Item = FilterField>) -> Self {
90 self.fields.extend(fields);
91 self
92 }
93
94 pub fn parameter(mut self, parameter: FilterField) -> Self {
96 self.parameters.push(parameter);
97 self
98 }
99
100 pub fn parameters(mut self, parameters: impl IntoIterator<Item = FilterField>) -> Self {
102 self.parameters.extend(parameters);
103 self
104 }
105
106 pub fn rows(mut self, rows: usize) -> Self {
108 self.rows = rows.max(1);
109 self
110 }
111
112 fn schema(&self) -> Schema {
114 let kind = |k: FilterFieldKind| match k {
115 FilterFieldKind::Text => FieldType::Text,
116 FilterFieldKind::Number => FieldType::Number,
117 FilterFieldKind::Boolean => FieldType::Boolean,
118 FilterFieldKind::Date => FieldType::Date,
119 };
120 let schema = self.fields.iter().fold(Schema::new(), |s, f| s.field(&f.name, kind(f.kind)));
121 self.parameters.iter().fold(schema, |s, p| s.parameter(&p.name, kind(p.kind)))
122 }
123
124 fn items(&self, group: usize, labels: &crate::Labels) -> Vec<Item> {
126 let named = |f: &FilterField, insert: String| Item {
127 title: f.caption.clone(),
128 insert,
129 back: 0,
130 signature: String::new(),
131 help: String::new(),
132 };
133 match group {
134 0 => self.fields.iter().map(|f| named(f, format!("[{}]", f.name))).collect(),
135 1 => self.parameters.iter().map(|p| named(p, format!("?{}", p.name))).collect(),
136 OPERATORS => labels
137 .expression_operators
138 .iter()
139 .map(|(op, help)| {
140 let (insert, back) = match op.as_str() {
141 "Between" | "In" => (format!(" {op}()"), 1),
142 _ => (format!(" {op} "), 0),
143 };
144 Item { title: op.clone(), insert, back, signature: String::new(), help: help.clone() }
145 })
146 .collect(),
147 CONSTANTS => labels
148 .expression_constants
149 .iter()
150 .map(|(value, help)| Item {
151 title: value.clone(),
152 insert: value.clone(),
153 back: usize::from(value == "''"),
154 signature: String::new(),
155 help: help.clone(),
156 })
157 .collect(),
158 g => {
159 let Some(category) = FUNCTION_GROUPS.get(g.wrapping_sub(2)) else { return Vec::new() };
160 let mut list: Vec<FunctionInfo> =
161 functions::catalog().into_iter().filter(|f| f.category == *category).collect();
162 list.sort_by_key(|f| f.name.to_lowercase());
163 list.dedup_by_key(|f| f.name);
164 list.into_iter()
165 .map(|f| Item {
166 title: f.name.to_owned(),
167 insert: format!("{}()", f.name),
168 back: 1,
169 signature: f.signature.to_owned(),
170 help: f.description.to_owned(),
171 })
172 .collect()
173 }
174 }
175 }
176}
177
178#[derive(Clone, Debug, PartialEq)]
180struct Item {
181 title: String,
182 insert: String,
184 back: usize,
187 signature: String,
188 help: String,
189}
190
191fn insert(value: &Binding<String>, cursor: &State<Cursor>, item: &Item) {
193 let text = value.get();
194 let chars = text.chars().count();
195 let c = cursor.get();
196 let caret = c.caret.min(chars);
197 let (start, end) = c.selection().map_or((caret, caret), |(a, b)| (a.min(chars), b.min(chars)));
198 let (a, b) = (byte_index(&text, start), byte_index(&text, end));
199 value.set(format!("{}{}{}", &text[..a], item.insert, &text[b..]));
200 cursor.set(Cursor::at(start + item.insert.chars().count() - item.back));
201}
202
203fn line_and_column(text: &str, offset: usize) -> (usize, usize) {
205 let before = &text[..offset.min(text.len())];
206 let line = before.matches('\n').count() + 1;
207 let column = before.rsplit('\n').next().map_or(0, |l| l.chars().count()) + 1;
208 (line, column)
209}
210
211fn problem_name(problem: &Problem) -> String {
213 match problem {
214 Problem::UnknownField(n) | Problem::UnknownCollection(n) => format!("[{n}]"),
215 Problem::UnknownParameter(n) => format!("?{n}"),
216 Problem::UnknownFunction(n) => format!("{n}()"),
217 other => other.to_string(),
218 }
219}
220
221#[cfg_attr(feature = "live", ravenrs_live::expose(part = "part1"))]
222impl ExpressionEditor {
223 fn status(&self, text: &str, labels: &crate::Labels) -> Option<Alert> {
225 if text.trim().is_empty() {
226 return None;
227 }
228 match Criteria::parse(text) {
229 Err(error) => {
230 let (line, column) = line_and_column(text, error.offset);
231 let at = labels
232 .expression_error_at
233 .replace("{line}", &line.to_string())
234 .replace("{column}", &column.to_string());
235 Some(Alert::new(at, error.message).tone(Tone::Danger).compact())
236 }
237 Ok(criteria) => {
238 let problems = validate(&criteria, &self.schema());
239 if problems.is_empty() {
240 Some(Alert::message(labels.expression_valid.clone()).tone(Tone::Success).compact())
241 } else {
242 let names: Vec<String> = problems.iter().map(problem_name).collect();
243 Some(
244 Alert::new(labels.expression_unknown.clone(), names.join(", "))
245 .tone(Tone::Warning)
246 .compact(),
247 )
248 }
249 }
250 }
251 }
252}
253
254impl View for ExpressionEditor {
255 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
256 let labels = cx.labels();
257 let cursor: State<Cursor> = cx.use_state(|| Cursor::at(usize::MAX));
258 let group: State<usize> = cx.use_state(|| 0);
259 let chosen: State<Option<usize>> = cx.use_state(|| None);
260 let query: State<String> = cx.use_state(String::new);
261
262 let text = self.value.get();
263 let status = self.status(&text, &labels);
264
265 let open = group.get();
267 let groups: Vec<AnyView> = labels
268 .expression_groups
269 .iter()
270 .enumerate()
271 .map(|(i, name)| {
272 let (g, c) = (group.clone(), chosen.clone());
273 Box::new(
274 SidebarItem::text(name.clone())
275 .compact()
276 .count(self.items(i, &labels).len().to_string())
277 .selected(i == open)
278 .on_click(move || {
279 g.set(i);
280 c.set(None);
281 }),
282 ) as AnyView
283 })
284 .collect();
285
286 let needle = query.get().trim().to_lowercase();
288 let pool: Vec<Item> = if needle.is_empty() {
289 self.items(open, &labels)
290 } else {
291 (0..labels.expression_groups.len()).flat_map(|g| self.items(g, &labels)).collect()
292 };
293 let shown: Vec<Item> = pool
294 .into_iter()
295 .filter(|item| {
296 needle.is_empty()
297 || item.title.to_lowercase().contains(&needle)
298 || item.insert.to_lowercase().contains(&needle)
299 })
300 .collect();
301 let picked = chosen.get().and_then(|i| shown.get(i)).cloned();
302 let rows: Vec<AnyView> = if shown.is_empty() {
303 vec![Box::new(
304 Text::new(labels.expression_no_match.clone())
305 .style(TextStyle::Callout)
306 .foreground(Semantic::InkMuted),
307 )]
308 } else {
309 shown
310 .iter()
311 .enumerate()
312 .map(|(i, item)| {
313 let (c, value, caret, it) =
314 (chosen.clone(), self.value.clone(), cursor.clone(), item.clone());
315 Box::new(
316 SidebarItem::text(item.title.clone())
317 .compact()
318 .selected(picked.as_ref() == Some(item) && chosen.get() == Some(i))
319 .on_click(move || {
320 if c.get() == Some(i) {
322 insert(&value, &caret, &it);
323 } else {
324 c.set(Some(i));
325 }
326 }),
327 ) as AnyView
328 })
329 .collect()
330 };
331
332 let description: AnyView = match &picked {
333 Some(item) => {
334 let (value, caret, it) = (self.value.clone(), cursor.clone(), item.clone());
335 let signature = if item.signature.is_empty() {
336 item.insert.trim().to_owned()
337 } else {
338 item.signature.clone()
339 };
340 Box::new(
341 VStack::new((
342 Text::new(signature).font_weight(Weight::SEMIBOLD),
343 (!item.help.is_empty()).then(|| {
344 Text::new(item.help.clone())
345 .style(TextStyle::Callout)
346 .foreground(Semantic::InkMuted)
347 }),
348 Button::new(labels.expression_insert.clone())
349 .icon(IconName::Plus)
350 .size(ButtonSize::Small)
351 .on_click(move || insert(&value, &caret, &it)),
352 ))
353 .spacing(SPACING.s2)
354 .align(Align::Start),
355 )
356 }
357 None => Box::new(VStack::new(Vec::<AnyView>::new())),
358 };
359
360 let editor = VStack::new((
361 TextArea::new(self.value.clone()).rows(self.rows).cursor(cursor.clone()),
362 status,
363 HStack::new((
364 ScrollView::new((VStack::new(groups).spacing(2.0).align(Align::Stretch),))
365 .width(GROUPS_WIDTH),
366 VStack::new((
367 TextField::new(query.binding())
368 .placeholder(labels.expression_search.clone())
369 .icon(IconName::Search)
370 .compact(),
371 ScrollView::new((VStack::new(rows).spacing(2.0).align(Align::Stretch),)).grow(1.0),
372 ))
373 .spacing(SPACING.s2)
374 .align(Align::Stretch)
375 .width(ITEMS_WIDTH),
376 ScrollView::new((description,)).grow(1.0),
377 ))
378 .spacing(SPACING.s3)
379 .align(Align::Stretch)
380 .height(LISTS_HEIGHT),
381 ))
382 .spacing(SPACING.s3)
383 .align(Align::Stretch);
384 cx.child(&editor)
385 }
386}