use crate::core::{Color, Font, HorizontalAlignment, Point, Rect, Size};
use crate::event::{Event, EventHandler};
use crate::render::RenderContext;
use crate::signal::Signal1;
use crate::widget::capability::coercion::{expect_string, expect_usize};
use crate::widget::capability::properties_trait::{base_property_get, base_property_set};
use crate::widget::capability::types::{CapabilityAccessError, CapabilityValue};
use crate::widget::capability::WidgetProperties;
use crate::widget::view_widgets::filter_expr::{FilterCondition, FilterExpr, FilterOperator};
use crate::widget::{BaseWidget, Draw, Widget, WidgetKind};
use crate::{impl_widget_property_hooks, property_names_of};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum FilterConjunction {
#[default]
And,
Or,
}
impl FilterConjunction {
pub fn as_str(self) -> &'static str {
match self {
FilterConjunction::And => "and",
FilterConjunction::Or => "or",
}
}
pub fn from_name(name: &str) -> Option<Self> {
Some(match name {
"and" => FilterConjunction::And,
"or" => FilterConjunction::Or,
_ => return None,
})
}
pub fn toggled(self) -> Self {
match self {
FilterConjunction::And => FilterConjunction::Or,
FilterConjunction::Or => FilterConjunction::And,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FilterField {
pub column: usize,
pub label: String,
pub operators: Vec<FilterOperator>,
}
impl FilterField {
pub fn text(column: usize, label: impl Into<String>) -> Self {
Self {
column,
label: label.into(),
operators: vec![
FilterOperator::Contains,
FilterOperator::Equals,
FilterOperator::StartsWith,
FilterOperator::EndsWith,
FilterOperator::NotContains,
],
}
}
pub fn number(column: usize, label: impl Into<String>) -> Self {
Self {
column,
label: label.into(),
operators: vec![
FilterOperator::EqualsNumber,
FilterOperator::GreaterThan,
FilterOperator::GreaterOrEqual,
FilterOperator::LessThan,
FilterOperator::LessOrEqual,
],
}
}
pub fn new(column: usize, label: impl Into<String>, operators: Vec<FilterOperator>) -> Self {
Self { column, label: label.into(), operators }
}
pub fn default_operator(&self) -> FilterOperator {
self.operators.first().copied().unwrap_or(FilterOperator::Contains)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct QueryBuilderRow {
pub field: usize,
pub operator: FilterOperator,
pub operand: String,
pub negated: bool,
}
impl QueryBuilderRow {
pub fn new(field: usize, operator: FilterOperator) -> Self {
Self { field, operator, operand: String::new(), negated: false }
}
}
const ROW_HEIGHT: u32 = 30;
const HEADER_HEIGHT: u32 = 34;
pub struct QueryBuilder {
base: BaseWidget,
fields: Vec<FilterField>,
rows: Vec<QueryBuilderRow>,
conjunction: FilterConjunction,
active_row: Option<usize>,
pub query_changed: Signal1<FilterExpr>,
}
impl QueryBuilder {
pub fn new(geometry: Rect) -> Self {
Self {
base: BaseWidget::new(WidgetKind::QueryBuilder, geometry, "QueryBuilder"),
fields: Vec::new(),
rows: Vec::new(),
conjunction: FilterConjunction::And,
active_row: None,
query_changed: Signal1::new(),
}
}
pub fn set_fields(&mut self, fields: Vec<FilterField>) {
self.fields = fields;
let field_count = self.fields.len();
self.rows.retain(|row| row.field < field_count);
self.base.request_redraw();
}
pub fn fields(&self) -> &[FilterField] {
&self.fields
}
pub fn rows(&self) -> &[QueryBuilderRow] {
&self.rows
}
pub fn row_count(&self) -> usize {
self.rows.len()
}
pub fn add_row(&mut self, field: usize) -> Option<usize> {
let default_operator = self.fields.get(field)?.default_operator();
let index = self.rows.len();
self.rows.push(QueryBuilderRow::new(field, default_operator));
self.active_row = Some(index);
self.emit_query();
Some(index)
}
pub fn remove_row(&mut self, index: usize) -> Option<QueryBuilderRow> {
if index >= self.rows.len() {
return None;
}
let removed = self.rows.remove(index);
self.active_row = match self.active_row {
Some(active) if active == index => None,
Some(active) if active > index => Some(active - 1),
other => other,
};
self.emit_query();
Some(removed)
}
pub fn swap_rows(&mut self, a: usize, b: usize) -> bool {
if a >= self.rows.len() || b >= self.rows.len() || a == b {
return false;
}
self.rows.swap(a, b);
self.emit_query();
true
}
pub fn set_row_operand(&mut self, index: usize, operand: impl Into<String>) -> bool {
match self.rows.get_mut(index) {
Some(row) => {
row.operand = operand.into();
self.emit_query();
true
}
None => false,
}
}
pub fn set_row_operator(&mut self, index: usize, operator: FilterOperator) -> bool {
let Some(row) = self.rows.get(index) else {
return false;
};
let field_index = row.field;
let offered =
self.fields.get(field_index).is_none_or(|field| field.operators.contains(&operator));
if !offered {
return false;
}
if let Some(row) = self.rows.get_mut(index) {
row.operator = operator;
}
self.emit_query();
true
}
pub fn set_row_negated(&mut self, index: usize, negated: bool) -> bool {
match self.rows.get_mut(index) {
Some(row) => {
row.negated = negated;
self.emit_query();
true
}
None => false,
}
}
pub fn conjunction(&self) -> FilterConjunction {
self.conjunction
}
pub fn set_conjunction(&mut self, conjunction: FilterConjunction) {
if self.conjunction == conjunction {
return;
}
self.conjunction = conjunction;
self.emit_query();
}
pub fn toggle_conjunction(&mut self) {
self.set_conjunction(self.conjunction.toggled());
}
pub fn active_row(&self) -> Option<usize> {
self.active_row
}
pub fn set_active_row(&mut self, index: usize) {
if index < self.rows.len() {
self.active_row = Some(index);
self.base.request_redraw();
}
}
pub fn incomplete_rows(&self) -> Vec<usize> {
self.rows
.iter()
.enumerate()
.filter(|(_, row)| row.operand.is_empty())
.map(|(index, _)| index)
.collect()
}
pub fn build_query(&self) -> FilterExpr {
let mut children = Vec::new();
for row in &self.rows {
if row.operand.is_empty() {
continue;
}
let Some(field) = self.fields.get(row.field) else {
continue;
};
let condition = FilterCondition::new(field.column, row.operator, row.operand.clone());
let predicate = FilterExpr::Predicate(condition);
children.push(if row.negated { FilterExpr::negate(predicate) } else { predicate });
}
match self.conjunction {
FilterConjunction::And => FilterExpr::and(children),
FilterConjunction::Or => FilterExpr::or(children),
}
}
pub fn set_query(&mut self, expr: &FilterExpr) -> bool {
let (conjunction, children) = match expr {
FilterExpr::And(children) => (FilterConjunction::And, children.as_slice()),
FilterExpr::Or(children) => (FilterConjunction::Or, children.as_slice()),
FilterExpr::MatchAll | FilterExpr::MatchNothing => {
self.rows.clear();
self.active_row = None;
self.conjunction = FilterConjunction::And;
return true;
}
other => (FilterConjunction::And, std::slice::from_ref(other)),
};
let mut rows = Vec::new();
let mut exact = true;
for child in children {
match Self::row_from_expr(child, &self.fields) {
Some(row) => rows.push(row),
None => {
exact = false;
}
}
}
self.rows = rows;
self.conjunction = conjunction;
self.active_row = None;
self.base.request_redraw();
exact
}
fn row_from_expr(expr: &FilterExpr, fields: &[FilterField]) -> Option<QueryBuilderRow> {
let (condition, negated) = match expr {
FilterExpr::Predicate(condition) => (condition, false),
FilterExpr::Not(inner) => match inner.as_ref() {
FilterExpr::Predicate(condition) => (condition, true),
_ => return None,
},
_ => return None,
};
let field = fields.iter().position(|field| field.column == condition.column)?;
Some(QueryBuilderRow {
field,
operator: condition.operator,
operand: condition.operand.clone(),
negated,
})
}
fn emit_query(&mut self) {
let query = self.build_query();
if self.query_changed.slot_count() > 0 {
self.query_changed.emit(query);
}
self.base.request_redraw();
}
fn row_rect(&self, index: usize) -> Option<Rect> {
if index >= self.rows.len() {
return None;
}
let rect = self.geometry();
Some(Rect::new(
rect.x,
rect.y + HEADER_HEIGHT as i32 + (index as i32) * ROW_HEIGHT as i32,
rect.width,
ROW_HEIGHT,
))
}
fn row_at(&self, pos: Point) -> Option<usize> {
(0..self.rows.len())
.find(|index| self.row_rect(*index).is_some_and(|rect| rect.contains_point(pos)))
}
}
impl Widget for QueryBuilder {
fn base(&self) -> &BaseWidget {
&self.base
}
fn base_mut(&mut self) -> &mut BaseWidget {
&mut self.base
}
fn size_hint(&self) -> Size {
Size::new(360, 120)
}
impl_draw_bridge!();
impl_widget_property_hooks!();
}
impl WidgetProperties for QueryBuilder {
fn get(&self, name: &str) -> Result<CapabilityValue, CapabilityAccessError> {
match name {
"row_count" => Ok(CapabilityValue::UInt(self.row_count() as u64)),
"incomplete_row_count" => {
Ok(CapabilityValue::UInt(self.incomplete_rows().len() as u64))
}
"field_count" => Ok(CapabilityValue::UInt(self.fields().len() as u64)),
"conjunction" => Ok(CapabilityValue::String(self.conjunction().as_str().to_string())),
"active_row" => Ok(match self.active_row() {
Some(index) => CapabilityValue::UInt(index as u64),
None => CapabilityValue::Null,
}),
_ => base_property_get(self, name),
}
}
fn set(&mut self, name: &str, value: CapabilityValue) -> Result<(), CapabilityAccessError> {
match name {
"conjunction" => {
let token = expect_string(value)?;
let Some(conjunction) = FilterConjunction::from_name(&token) else {
return Err(CapabilityAccessError::TypeMismatch);
};
self.set_conjunction(conjunction);
Ok(())
}
"active_row" => {
self.set_active_row(expect_usize(value)?);
Ok(())
}
"row_count" | "incomplete_row_count" | "field_count" => {
Err(CapabilityAccessError::ReadOnlyProperty)
}
_ => base_property_set(self, name, value),
}
}
fn property_names(&self) -> &'static [&'static str] {
property_names_of![
"row_count",
"incomplete_row_count",
"field_count",
"conjunction",
"active_row",
BASE_PROPERTY_NAMES
]
}
}
impl Draw for QueryBuilder {
fn draw(&mut self, context: &mut RenderContext) {
let rect = self.geometry();
if rect.width == 0 || rect.height == 0 {
return;
}
context.fill_rect(rect, Color::rgb(250, 250, 252));
context.draw_rect(rect, Color::rgb(210, 212, 218));
self.draw_header(context, rect);
for index in 0..self.rows.len() {
self.draw_row(context, index);
}
if self.rows.is_empty() {
context.draw_text(
Point::new(rect.x + 10, rect.y + HEADER_HEIGHT as i32 + 20),
"No conditions",
&Font::simple("Sans", 11.0),
Color::rgb(150, 154, 162),
HorizontalAlignment::Left,
);
}
}
}
impl QueryBuilder {
fn draw_header(&self, context: &mut RenderContext, rect: Rect) {
let header = Rect::new(rect.x, rect.y, rect.width, HEADER_HEIGHT);
context.fill_rect(header, Color::rgb(238, 240, 244));
let chip = Rect::new(rect.x + 8, rect.y + 7, 56, 20);
let chip_color = match self.conjunction {
FilterConjunction::And => Color::rgb(66, 133, 244),
FilterConjunction::Or => Color::rgb(244, 150, 60),
};
context.fill_rounded_rect(chip, 10, chip_color);
context.draw_text(
Point::new(chip.x + 10, chip.y + 14),
self.conjunction.as_str().to_uppercase().as_str(),
&Font::simple("Sans", 11.0),
Color::WHITE,
HorizontalAlignment::Left,
);
context.draw_text(
Point::new(chip.x + chip.width as i32 + 10, rect.y + 21),
&format!("{} condition(s)", self.rows.len()),
&Font::simple("Sans", 11.0),
Color::rgb(90, 94, 102),
HorizontalAlignment::Left,
);
let add = Rect::new(rect.x + rect.width as i32 - 30, rect.y + 7, 22, 20);
context.fill_rounded_rect(add, 4, Color::rgb(220, 224, 232));
context.draw_text(
Point::new(add.x + 7, add.y + 14),
"+",
&Font::simple("Sans", 14.0),
Color::rgb(60, 64, 72),
HorizontalAlignment::Left,
);
}
fn draw_row(&self, context: &mut RenderContext, index: usize) {
let Some(row_rect) = self.row_rect(index) else {
return;
};
let Some(row) = self.rows.get(index) else {
return;
};
let active = self.active_row == Some(index);
if active {
context.fill_rect(row_rect, Color::rgb(235, 242, 254));
}
context.draw_line_stroke(
Point::new(row_rect.x, row_rect.y),
Point::new(row_rect.x + row_rect.width as i32, row_rect.y),
Color::rgb(228, 230, 236),
1,
);
let mut x = row_rect.x + 8;
if row.negated {
context.draw_text(
Point::new(x, row_rect.y + 19),
"!",
&Font::simple("Sans", 13.0),
Color::rgb(219, 68, 55),
HorizontalAlignment::Left,
);
}
x += 16;
let field_label =
self.fields.get(row.field).map_or("(field)".to_string(), |field| field.label.clone());
context.draw_text(
Point::new(x, row_rect.y + 19),
&field_label,
&Font::simple("Sans", 11.0),
Color::rgb(40, 44, 52),
HorizontalAlignment::Left,
);
x += 84;
context.draw_text(
Point::new(x, row_rect.y + 19),
row.operator.as_str(),
&Font::simple("Sans", 10.0),
Color::rgb(110, 116, 126),
HorizontalAlignment::Left,
);
x += 96;
let operand_box = Rect::new(x, row_rect.y + 6, row_rect.width.saturating_sub(200), 18);
context.fill_rect(operand_box, Color::WHITE);
context.draw_rect(operand_box, Color::rgb(200, 204, 212));
let (text, color) = if row.operand.is_empty() {
("type a value".to_string(), Color::rgb(160, 164, 172))
} else {
(row.operand.clone(), Color::rgb(40, 44, 52))
};
context.draw_text(
Point::new(operand_box.x + 5, operand_box.y + 13),
&text,
&Font::simple("Sans", 11.0),
color,
HorizontalAlignment::Left,
);
let remove = Rect::new(row_rect.x + row_rect.width as i32 - 24, row_rect.y + 8, 16, 16);
context.draw_line_stroke(
Point::new(remove.x + 3, remove.y + 3),
Point::new(remove.x + 13, remove.y + 13),
Color::rgb(150, 154, 162),
1,
);
context.draw_line_stroke(
Point::new(remove.x + 13, remove.y + 3),
Point::new(remove.x + 3, remove.y + 13),
Color::rgb(150, 154, 162),
1,
);
}
}
impl EventHandler for QueryBuilder {
fn handle_event(&mut self, event: &Event) {
self.base.handle_event(event);
if !self.base.is_enabled() {
return;
}
match event {
Event::MousePress { pos, button } if *button == 1 => {
let rect = self.geometry();
let header = Rect::new(rect.x, rect.y, rect.width, HEADER_HEIGHT);
if header.contains_point(*pos) {
let add = Rect::new(rect.x + rect.width as i32 - 30, rect.y + 7, 22, 20);
if add.contains_point(*pos) {
if !self.fields.is_empty() {
self.add_row(0);
}
} else {
self.toggle_conjunction();
}
return;
}
if let Some(index) = self.row_at(*pos) {
let Some(row_rect) = self.row_rect(index) else {
return;
};
let remove =
Rect::new(row_rect.x + row_rect.width as i32 - 24, row_rect.y + 8, 16, 16);
if remove.contains_point(*pos) {
self.remove_row(index);
} else {
self.set_active_row(index);
}
}
}
Event::TextInput { text } if self.active_row.is_some() => {
if text.chars().all(|ch| !ch.is_control()) {
if let Some(index) = self.active_row {
if let Some(row) = self.rows.get_mut(index) {
row.operand.push_str(text);
}
self.emit_query();
}
}
}
Event::KeyDown((8, _)) if self.active_row.is_some() => {
if let Some(index) = self.active_row {
if let Some(row) = self.rows.get_mut(index) {
row.operand.pop();
}
self.emit_query();
}
}
Event::KeyDown((38, _)) if self.active_row.is_some() => {
if let Some(index) = self.active_row {
self.set_active_row(index.saturating_sub(1));
}
}
Event::KeyDown((40, _)) if self.active_row.is_some() => {
if let Some(index) = self.active_row {
let last = self.rows.len().saturating_sub(1);
self.set_active_row((index + 1).min(last));
}
}
_ => {}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::render::{PaintBackend, SoftwarePaintBackend};
fn fields() -> Vec<FilterField> {
vec![
FilterField::text(0, "Name"),
FilterField::number(1, "Amount"),
FilterField::text(2, "Status"),
]
}
fn builder() -> QueryBuilder {
let mut b = QueryBuilder::new(Rect::new(0, 0, 360, 160));
b.set_fields(fields());
b
}
fn render(b: &mut QueryBuilder, size: Size) -> Vec<u8> {
let mut backend = SoftwarePaintBackend::new(size, 1.0);
backend.begin_frame(Color::WHITE);
let mut context = RenderContext::new(&mut backend);
b.draw(&mut context);
backend.end_frame();
backend.frame_rgba().to_vec()
}
fn row(a: &str, b: &str, c: &str) -> Vec<Option<String>> {
vec![Some(a.to_string()), Some(b.to_string()), Some(c.to_string())]
}
#[test]
fn query_builder_creation_defaults() {
let b = QueryBuilder::new(Rect::new(0, 0, 360, 160));
assert_eq!(b.kind(), WidgetKind::QueryBuilder);
assert!(b.fields().is_empty());
assert_eq!(b.row_count(), 0);
assert_eq!(b.conjunction(), FilterConjunction::And);
assert_eq!(b.active_row(), None);
}
#[test]
fn query_builder_add_row_uses_the_field_default_operator() {
let mut b = builder();
assert_eq!(b.add_row(0), Some(0), "a text field defaults to contains");
assert_eq!(b.add_row(1), Some(1), "a numeric field defaults to its first operator");
assert_eq!(b.rows()[0].operator, FilterOperator::Contains);
assert_eq!(b.rows()[1].operator, FilterOperator::EqualsNumber);
}
#[test]
fn query_builder_add_row_rejects_an_unknown_field() {
let mut b = builder();
assert_eq!(b.add_row(9), None);
assert_eq!(b.row_count(), 0);
}
#[test]
fn query_builder_a_new_row_becomes_active() {
let mut b = builder();
b.add_row(0);
assert_eq!(b.active_row(), Some(0), "the user's next act is to type the operand");
b.add_row(1);
assert_eq!(b.active_row(), Some(1));
}
#[test]
fn query_builder_remove_row_shifts_the_active_index() {
let mut b = builder();
b.add_row(0);
b.add_row(1);
b.add_row(2);
b.set_active_row(2);
assert!(b.remove_row(0).is_some());
assert_eq!(b.active_row(), Some(1));
assert!(b.remove_row(1).is_some());
assert_eq!(b.active_row(), None);
}
#[test]
fn query_builder_remove_row_rejects_an_unknown_index() {
let mut b = builder();
assert!(b.remove_row(0).is_none());
}
#[test]
fn query_builder_set_fields_drops_rows_pointing_past_the_new_list() {
let mut b = builder();
b.add_row(2);
b.set_row_operand(0, "open");
assert_eq!(b.row_count(), 1);
b.set_fields(vec![FilterField::text(0, "A"), FilterField::text(1, "B")]);
assert_eq!(b.row_count(), 0, "a row on a vanished field is dropped");
}
#[test]
fn query_builder_swap_rows_reorders() {
let mut b = builder();
b.add_row(0);
b.add_row(1);
b.set_row_operand(0, "first");
b.set_row_operand(1, "second");
assert!(b.swap_rows(0, 1));
assert_eq!(b.rows()[0].operand, "second");
assert_eq!(b.rows()[1].operand, "first");
assert!(!b.swap_rows(0, 0));
assert!(!b.swap_rows(0, 9));
}
#[test]
fn query_builder_set_row_operand_rejects_an_unknown_row() {
let mut b = builder();
assert!(!b.set_row_operand(0, "x"));
b.add_row(0);
assert!(b.set_row_operand(0, "x"));
assert_eq!(b.rows()[0].operand, "x");
}
#[test]
fn query_builder_set_row_operator_refuses_one_the_field_does_not_offer() {
let mut b = builder();
b.add_row(0); assert!(b.set_row_operator(0, FilterOperator::StartsWith));
assert!(!b.set_row_operator(0, FilterOperator::GreaterThan));
assert_eq!(b.rows()[0].operator, FilterOperator::StartsWith);
}
#[test]
fn query_builder_set_row_negated_round_trips() {
let mut b = builder();
b.add_row(0);
assert!(!b.rows()[0].negated);
assert!(b.set_row_negated(0, true));
assert!(b.rows()[0].negated);
assert!(b.set_row_negated(0, false));
assert!(!b.rows()[0].negated);
assert!(!b.set_row_negated(9, true));
}
#[test]
fn query_builder_toggle_conjunction_switches() {
let mut b = builder();
assert_eq!(b.conjunction(), FilterConjunction::And);
b.toggle_conjunction();
assert_eq!(b.conjunction(), FilterConjunction::Or);
b.set_conjunction(FilterConjunction::Or);
assert_eq!(b.conjunction(), FilterConjunction::Or, "setting the same value is a no-op");
}
#[test]
fn query_builder_empty_rows_produce_no_filter() {
let b = builder();
assert_eq!(b.build_query(), FilterExpr::MatchAll);
assert!(b.build_query().is_noop());
}
#[test]
fn query_builder_one_row_produces_one_predicate() {
let mut b = builder();
b.add_row(0);
b.set_row_operand(0, "alpha");
let query = b.build_query();
assert_eq!(query, FilterExpr::Predicate(FilterCondition::contains(0, "alpha")));
assert!(query.accepts(&row("Alpha", "", "")));
assert!(!query.accepts(&row("Beta", "", "")));
}
#[test]
fn query_builder_rows_combine_with_the_conjunction() {
let mut b = builder();
b.add_row(0);
b.set_row_operand(0, "a");
b.add_row(2);
b.set_row_operand(1, "open");
let query = b.build_query();
assert!(query.accepts(&row("aaa", "", "open")));
assert!(!query.accepts(&row("aaa", "", "closed")));
assert!(!query.accepts(&row("zzz", "", "open")));
b.set_conjunction(FilterConjunction::Or);
let query = b.build_query();
assert!(query.accepts(&row("aaa", "", "closed")));
assert!(query.accepts(&row("zzz", "", "open")));
assert!(!query.accepts(&row("zzz", "", "closed")));
}
#[test]
fn query_builder_a_row_with_no_operand_is_excluded_from_the_query() {
let mut b = builder();
b.add_row(0);
b.set_row_operand(0, "alpha");
b.add_row(2); b.add_row(1);
let query = b.build_query();
assert_eq!(query.condition_count(), 1, "only the complete row is in the query");
assert!(query.accepts(&row("Alpha", "", "")));
assert_eq!(b.incomplete_rows(), vec![1, 2]);
}
#[test]
fn query_builder_a_numeric_row_uses_its_operator() {
let mut b = builder();
b.add_row(1); b.set_row_operator(0, FilterOperator::GreaterThan);
b.set_row_operand(0, "100");
let query = b.build_query();
assert!(query.accepts(&row("", "150", "")));
assert!(!query.accepts(&row("", "50", "")));
}
#[test]
fn query_builder_a_negated_row_inverts_just_that_row() {
let mut b = builder();
b.add_row(0);
b.set_row_operand(0, "alpha");
b.set_row_negated(0, true);
b.add_row(2);
b.set_row_operand(1, "open");
let query = b.build_query();
assert!(query.accepts(&row("Beta", "", "open")));
assert!(!query.accepts(&row("Alpha", "", "open")));
assert!(!query.accepts(&row("Beta", "", "closed")));
}
#[test]
fn query_builder_query_changed_signal_fires_on_every_edit() {
let mut b = builder();
let seen = std::sync::Arc::new(std::sync::Mutex::new(Vec::<usize>::new()));
let sink = seen.clone();
b.query_changed.connect(move |query| {
if let Ok(mut guard) = sink.lock() {
guard.push(query.condition_count());
}
});
b.add_row(0); b.set_row_operand(0, "x"); b.add_row(2); b.set_row_operand(1, "y"); b.remove_row(0);
assert_eq!(*seen.lock().expect("signal lock poisoned"), vec![0, 1, 1, 2, 1]);
}
#[test]
fn query_builder_set_query_round_trips_a_flat_conjunction() {
let mut b = builder();
b.add_row(0);
b.set_row_operand(0, "alpha");
b.add_row(2);
b.set_row_operand(1, "open");
let built = b.build_query();
let mut fresh = builder();
assert!(fresh.set_query(&built), "a flat conjunction round-trips exactly");
assert_eq!(fresh.row_count(), 2);
assert_eq!(fresh.rows()[0].operand, "alpha");
assert_eq!(fresh.rows()[1].operand, "open");
assert_eq!(fresh.build_query(), built);
}
#[test]
fn query_builder_set_query_round_trips_a_negated_row() {
let mut b = builder();
b.add_row(0);
b.set_row_operand(0, "alpha");
b.set_row_negated(0, true);
let built = b.build_query();
let mut fresh = builder();
assert!(fresh.set_query(&built));
assert!(fresh.rows()[0].negated, "the negation must survive the round trip");
assert_eq!(fresh.build_query(), built);
}
#[test]
fn query_builder_set_query_round_trips_the_conjunction() {
let mut b = builder();
b.add_row(0);
b.set_row_operand(0, "a");
b.add_row(2);
b.set_row_operand(1, "b");
b.set_conjunction(FilterConjunction::Or);
let built = b.build_query();
let mut fresh = builder();
assert!(fresh.set_query(&built));
assert_eq!(fresh.conjunction(), FilterConjunction::Or);
assert_eq!(fresh.build_query(), built);
}
#[test]
fn query_builder_set_query_reports_a_nesting_it_cannot_represent() {
let mut b = builder();
let nested = FilterExpr::or(vec![
FilterExpr::and(vec![
FilterExpr::Predicate(FilterCondition::contains(0, "a")),
FilterExpr::Predicate(FilterCondition::contains(1, "5")),
]),
FilterExpr::Predicate(FilterCondition::contains(2, "open")),
]);
assert!(!b.set_query(&nested), "a nested group is not representable");
}
#[test]
fn query_builder_set_query_on_match_all_empties_the_rows() {
let mut b = builder();
b.add_row(0);
b.set_row_operand(0, "x");
assert!(b.set_query(&FilterExpr::MatchAll));
assert_eq!(b.row_count(), 0);
assert_eq!(b.build_query(), FilterExpr::MatchAll);
}
#[test]
fn query_builder_set_query_skips_a_condition_on_an_undeclared_column() {
let mut b = builder();
let expr = FilterExpr::Predicate(FilterCondition::contains(7, "x"));
assert!(!b.set_query(&expr));
assert_eq!(b.row_count(), 0);
}
#[test]
fn query_builder_click_selects_a_row() {
let mut b = builder();
b.add_row(0);
b.add_row(2);
b.active_row = None;
let rect = b.row_rect(1).expect("row 1");
b.handle_event(&Event::mouse_press(rect.x + 60, rect.y + 10, 1));
assert_eq!(b.active_row(), Some(1));
}
#[test]
fn query_builder_click_on_remove_deletes_the_row() {
let mut b = builder();
b.add_row(0);
b.add_row(2);
let rect = b.row_rect(0).expect("row 0");
let remove_x = rect.x + rect.width as i32 - 16;
b.handle_event(&Event::mouse_press(remove_x, rect.y + 16, 1));
assert_eq!(b.row_count(), 1);
}
#[test]
fn query_builder_click_on_the_header_toggles_the_conjunction() {
let mut b = builder();
b.handle_event(&Event::mouse_press(20, 16, 1));
assert_eq!(b.conjunction(), FilterConjunction::Or);
}
#[test]
fn query_builder_click_on_add_appends_a_row() {
let mut b = builder();
let rect = b.geometry();
let add_x = rect.x + rect.width as i32 - 20;
b.handle_event(&Event::mouse_press(add_x, rect.y + 16, 1));
assert_eq!(b.row_count(), 1);
}
#[test]
fn query_builder_typing_goes_to_the_active_row() {
let mut b = builder();
b.add_row(0);
b.handle_event(&Event::TextInput { text: "al".to_string() });
b.handle_event(&Event::TextInput { text: "pha".to_string() });
assert_eq!(b.rows()[0].operand, "alpha");
b.handle_event(&Event::KeyDown((8, 0)));
assert_eq!(b.rows()[0].operand, "alph");
}
#[test]
fn query_builder_typing_without_an_active_row_is_ignored() {
let mut b = builder();
b.add_row(0);
b.active_row = None;
b.handle_event(&Event::TextInput { text: "x".to_string() });
assert_eq!(b.rows()[0].operand, "");
}
#[test]
fn query_builder_arrows_move_the_active_row() {
let mut b = builder();
b.add_row(0);
b.add_row(2);
b.add_row(1);
b.set_active_row(1);
b.handle_event(&Event::KeyDown((40, 0)));
assert_eq!(b.active_row(), Some(2));
b.handle_event(&Event::KeyDown((40, 0)));
assert_eq!(b.active_row(), Some(2));
b.handle_event(&Event::KeyDown((38, 0)));
assert_eq!(b.active_row(), Some(1));
}
#[test]
fn query_builder_disabled_ignores_clicks() {
let mut b = builder();
b.set_enabled(false);
b.handle_event(&Event::mouse_press(20, 16, 1));
assert_eq!(b.conjunction(), FilterConjunction::And, "a disabled builder must not toggle");
}
#[test]
fn query_builder_draw_empty_paints_a_hint() {
let mut b = builder();
let empty = render(&mut b, Size::new(360, 160));
b.add_row(0);
let with_row = render(&mut b, Size::new(360, 160));
assert_ne!(empty, with_row, "a row must be visible");
}
#[test]
fn query_builder_draw_zero_geometry_does_not_panic() {
let mut b = builder();
b.add_row(0);
let rgba = render(&mut b, Size::new(4, 4));
assert!(!rgba.is_empty());
}
#[test]
fn query_builder_active_row_is_visible() {
let mut b = builder();
b.add_row(0);
b.active_row = None;
let inactive = render(&mut b, Size::new(360, 160));
b.set_active_row(0);
let active = render(&mut b, Size::new(360, 160));
assert_ne!(inactive, active, "the active row must be highlighted");
}
#[test]
fn query_builder_conjunction_property_round_trips() {
let mut b = builder();
assert_eq!(b.get("conjunction").unwrap(), CapabilityValue::String("and".to_string()));
b.set("conjunction", CapabilityValue::String("or".to_string())).unwrap();
assert_eq!(b.conjunction(), FilterConjunction::Or);
assert!(b.set("conjunction", CapabilityValue::String("xor".to_string())).is_err());
}
#[test]
fn query_builder_active_row_property_round_trips() {
let mut b = builder();
b.add_row(0);
assert_eq!(b.get("active_row").unwrap(), CapabilityValue::UInt(0));
b.set("active_row", CapabilityValue::UInt(0)).unwrap();
assert_eq!(b.active_row(), Some(0));
b.set("active_row", CapabilityValue::UInt(9)).unwrap();
assert_eq!(b.active_row(), Some(0));
}
#[test]
fn query_builder_derived_properties_are_read_only() {
let mut b = builder();
b.add_row(0);
assert_eq!(b.get("row_count").unwrap(), CapabilityValue::UInt(1));
assert_eq!(b.get("field_count").unwrap(), CapabilityValue::UInt(3));
assert_eq!(b.get("incomplete_row_count").unwrap(), CapabilityValue::UInt(1));
for name in ["row_count", "incomplete_row_count", "field_count"] {
assert_eq!(
b.set(name, CapabilityValue::UInt(9)),
Err(CapabilityAccessError::ReadOnlyProperty),
"{name} must be read-only"
);
}
}
#[test]
fn filter_conjunction_round_trips_and_toggles() {
for conjunction in [FilterConjunction::And, FilterConjunction::Or] {
assert_eq!(FilterConjunction::from_name(conjunction.as_str()), Some(conjunction));
assert_ne!(conjunction.toggled(), conjunction);
}
assert_eq!(FilterConjunction::from_name("nand"), None);
assert_eq!(FilterConjunction::default(), FilterConjunction::And);
}
#[test]
fn filter_field_constructors_offer_the_right_operators() {
let text = FilterField::text(0, "Name");
assert_eq!(text.default_operator(), FilterOperator::Contains);
assert!(text.operators.contains(&FilterOperator::StartsWith));
assert!(!text.operators.contains(&FilterOperator::GreaterThan));
let number = FilterField::number(1, "Amount");
assert_eq!(number.default_operator(), FilterOperator::EqualsNumber);
assert!(number.operators.contains(&FilterOperator::GreaterThan));
assert!(!number.operators.contains(&FilterOperator::StartsWith));
let bare = FilterField::new(2, "Bare", Vec::new());
assert_eq!(bare.default_operator(), FilterOperator::Contains);
}
}