use crate::spatial_cache::{CanvasSpatialCache, IndexedHitRecord};
use crate::{
CanvasDocument, CanvasDocumentDiff, CanvasEdgeRouter, CanvasGeometryFacts, CanvasGraphIndex,
CanvasKindRegistry, CanvasResolvedEdgeGeometry, EdgeId, HitOptions, HitRecord, HitTarget,
};
use indexmap::IndexMap;
use open_gpui::{Bounds, Pixels, Point};
use std::cmp::Ordering;
#[derive(Clone, Debug, Default, PartialEq)]
pub(crate) struct CanvasRuntimeQuery {
spatial_cache: CanvasSpatialCache,
}
impl CanvasRuntimeQuery {
pub(crate) fn rebuild_with_router<R>(document: &CanvasDocument, router: &R) -> Self
where
R: CanvasEdgeRouter + ?Sized,
{
Self::rebuild_with_router_and_optional_kind_registry(document, router, None)
}
pub(crate) fn rebuild_with_router_and_kind_registry<R>(
document: &CanvasDocument,
router: &R,
kind_registry: &CanvasKindRegistry,
) -> Self
where
R: CanvasEdgeRouter + ?Sized,
{
Self::rebuild_with_router_and_optional_kind_registry(document, router, Some(kind_registry))
}
fn rebuild_with_router_and_optional_kind_registry<R>(
document: &CanvasDocument,
router: &R,
kind_registry: Option<&CanvasKindRegistry>,
) -> Self
where
R: CanvasEdgeRouter + ?Sized,
{
let spatial_cache = match kind_registry {
Some(kind_registry) => CanvasSpatialCache::rebuild_with_router_and_kind_registry(
document,
router,
kind_registry,
),
None => CanvasSpatialCache::rebuild_with_router(document, router),
};
Self { spatial_cache }
}
pub(crate) fn apply_diff_with_graph_index_and_router<R>(
&mut self,
document: &CanvasDocument,
diff: &CanvasDocumentDiff,
graph_index: &CanvasGraphIndex,
router: &R,
) where
R: CanvasEdgeRouter + ?Sized,
{
self.spatial_cache.apply_diff_with_graph_index_and_router(
document,
diff,
graph_index,
router,
);
}
pub(crate) fn apply_diff_with_graph_index_router_and_kind_registry<R>(
&mut self,
document: &CanvasDocument,
diff: &CanvasDocumentDiff,
graph_index: &CanvasGraphIndex,
router: &R,
kind_registry: &CanvasKindRegistry,
) where
R: CanvasEdgeRouter + ?Sized,
{
self.spatial_cache
.apply_diff_with_graph_index_router_and_kind_registry(
document,
diff,
graph_index,
router,
kind_registry,
);
}
pub(crate) fn query(&self, viewport: Bounds<Pixels>) -> impl Iterator<Item = &HitRecord> {
self.query_with_options(
viewport,
HitOptions {
include_locked: true,
..HitOptions::default()
},
)
}
pub(crate) fn query_with_options(
&self,
viewport: Bounds<Pixels>,
options: HitOptions,
) -> impl Iterator<Item = &HitRecord> {
let mut records = self
.spatial_cache
.query_candidates(viewport)
.filter(move |record| query_matches(&record.record, viewport, options))
.collect::<Vec<_>>();
records.sort_by(|left, right| compare_query_records(left, right));
records.into_iter().map(|record| &record.record)
}
pub(crate) fn hit_test(
&self,
point: Point<Pixels>,
options: HitOptions,
) -> impl Iterator<Item = &HitRecord> {
let mut records = self
.spatial_cache
.hit_test_candidates(point, options.margin)
.filter(move |record| hit_matches(&record.record, point, options))
.collect::<Vec<_>>();
records.sort_by(|left, right| compare_hit_records(left, right));
records.into_iter().map(|record| &record.record)
}
pub(crate) fn precise_hit_test_with_facts<'a, R>(
&'a self,
facts: CanvasGeometryFacts<'a, R>,
edge_geometries: &'a IndexMap<EdgeId, CanvasResolvedEdgeGeometry>,
point: Point<Pixels>,
options: HitOptions,
) -> impl Iterator<Item = &'a HitRecord> + 'a
where
R: CanvasEdgeRouter + 'a,
{
self.hit_test(point, options).filter(move |record| {
let edge_geometry = match &record.target {
HitTarget::Edge(id) => edge_geometries.get(id),
_ => None,
};
facts.record_contains_point_with_edge_geometry(record, point, options, edge_geometry)
})
}
}
pub(crate) fn query_matches(
record: &HitRecord,
viewport: Bounds<Pixels>,
options: HitOptions,
) -> bool {
options_match(record, options) && record.bounds.intersects(&viewport)
}
pub(crate) fn hit_matches(record: &HitRecord, point: Point<Pixels>, options: HitOptions) -> bool {
if !options_match(record, options) {
return false;
}
let bounds = if options.margin == Pixels::ZERO {
record.bounds
} else {
record.bounds.dilate(options.margin)
};
bounds.contains(&point)
}
fn compare_query_records(left: &IndexedHitRecord, right: &IndexedHitRecord) -> Ordering {
left.record
.z_index
.cmp(&right.record.z_index)
.then_with(|| left.ordinal.cmp(&right.ordinal))
}
fn compare_hit_records(left: &IndexedHitRecord, right: &IndexedHitRecord) -> Ordering {
right
.record
.z_index
.cmp(&left.record.z_index)
.then_with(|| right.ordinal.cmp(&left.ordinal))
}
fn options_match(record: &HitRecord, options: HitOptions) -> bool {
(options.include_hidden || !record.hidden)
&& (options.include_locked || !record.locked)
&& (options.include_handles || !matches!(record.target, HitTarget::Handle { .. }))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_support::document_fixture;
use crate::{
CanvasDefaultEdgeRouter, CanvasHandle, CanvasNode, CanvasShape, CanvasTransaction,
DocumentCommand, NodeId,
};
use open_gpui::{point, px, size};
#[test]
fn runtime_query_owns_filtering_and_ordering_over_cache_candidates() {
let mut back = CanvasNode::new("back", point(px(0.0), px(0.0)), size(px(100.0), px(100.0)));
back.z_index = 1;
back.handles
.push(CanvasHandle::new("out", point(px(95.0), px(50.0))));
let mut front = CanvasShape::new(
"front",
Bounds::new(point(px(0.0), px(0.0)), size(px(100.0), px(100.0))),
);
front.z_index = 2;
let document = document_fixture().node(back).shape(front).build();
let query = CanvasRuntimeQuery::rebuild_with_router(&document, &CanvasDefaultEdgeRouter);
assert_eq!(
query
.hit_test(point(px(95.0), px(50.0)), HitOptions::default())
.map(|record| record.target.clone())
.collect::<Vec<_>>(),
vec![
HitTarget::Shape("front".into()),
HitTarget::Node("back".into())
]
);
assert_eq!(
query
.hit_test(
point(px(95.0), px(50.0)),
HitOptions {
include_handles: true,
..HitOptions::default()
},
)
.map(|record| record.target.clone())
.collect::<Vec<_>>(),
vec![
HitTarget::Shape("front".into()),
HitTarget::Handle {
node_id: "back".into(),
handle_id: "out".into(),
},
HitTarget::Node("back".into()),
]
);
}
#[test]
fn runtime_query_suppresses_stale_cache_records_after_diff() {
let mut document = document_fixture()
.node(CanvasNode::new(
"a",
point(px(0.0), px(0.0)),
size(px(100.0), px(100.0)),
))
.build();
let mut query =
CanvasRuntimeQuery::rebuild_with_router(&document, &CanvasDefaultEdgeRouter);
let mut moved = document.node(&NodeId::from("a")).unwrap().clone();
moved.position = point(px(200.0), px(0.0));
let diff = document
.apply_transaction_with_diff(CanvasTransaction::single(DocumentCommand::UpdateNode(
moved,
)))
.unwrap();
let graph_index = document.graph_index();
query.apply_diff_with_graph_index_and_router(
&document,
&diff,
&graph_index,
&CanvasDefaultEdgeRouter,
);
assert!(
query
.hit_test(point(px(10.0), px(10.0)), HitOptions::default())
.next()
.is_none()
);
assert!(
query
.hit_test(point(px(210.0), px(10.0)), HitOptions::default())
.any(|record| record.target == HitTarget::Node(NodeId::from("a")))
);
}
}