Skip to main content

egui_charts/drawings/
repositories.rs

1//! Drawing tool category lookup and search.
2//!
3//! [`DrawingToolsRepository`] supplies category sections and case-insensitive tool
4//! search for host pickers. [`InMemoryToolsRepository`] reads the crate's static
5//! toolbar category definitions in [`categories`], rather than the finer
6//! command-list groups of [`DrawingToolType::categories`]. Measure is a standalone
7//! toolbar button, so it is not included in this repository's category search.
8
9use crate::drawings::DrawingToolType;
10use crate::drawings::categories;
11
12/// Repository trait for drawing tools data access.
13///
14/// This trait defines the contract for data access. Different implementations
15/// can provide data from different sources (static data, JSON files, databases, etc.)
16pub trait DrawingToolsRepository {
17    /// Get all available category names in display order
18    fn get_all_categories(&self) -> Vec<String>;
19
20    /// Get all tools for a specific category, organized by sections
21    ///
22    /// Returns: Vec<(section_name, tools_in_section)>
23    fn get_category_tools(&self, category: &str) -> Vec<(String, Vec<DrawingToolType>)>;
24
25    /// Get the category that a tool belongs to
26    fn get_tool_category(&self, tool: DrawingToolType) -> Option<String>;
27
28    /// Search tools by query string (case-insensitive)
29    fn search_tools(&self, query: &str) -> Vec<DrawingToolType>;
30}
31
32/// Drawing tool lookup backed by [`categories`].
33pub struct InMemoryToolsRepository;
34
35impl InMemoryToolsRepository {
36    /// Create a new in-memory repository
37    pub fn new() -> Self {
38        Self
39    }
40}
41
42impl Default for InMemoryToolsRepository {
43    fn default() -> Self {
44        Self::new()
45    }
46}
47
48impl DrawingToolsRepository for InMemoryToolsRepository {
49    fn get_all_categories(&self) -> Vec<String> {
50        categories::CATEGORY_ORDER
51            .iter()
52            .map(|category| category.to_string())
53            .collect()
54    }
55
56    fn get_category_tools(&self, category: &str) -> Vec<(String, Vec<DrawingToolType>)> {
57        categories::get_category_sections(category)
58            .into_iter()
59            .map(|(section, tools)| (section.to_string(), tools))
60            .collect()
61    }
62
63    fn get_tool_category(&self, tool: DrawingToolType) -> Option<String> {
64        categories::get_tool_category(tool).map(|s| s.to_string())
65    }
66
67    fn search_tools(&self, query: &str) -> Vec<DrawingToolType> {
68        if query.is_empty() {
69            return self.get_all_tools();
70        }
71
72        let query_lower = query.to_lowercase();
73        self.get_all_tools()
74            .into_iter()
75            .filter(|tool| tool.as_str().to_lowercase().contains(&query_lower))
76            .collect()
77    }
78}
79
80impl InMemoryToolsRepository {
81    /// Get all available tools from all categories
82    fn get_all_tools(&self) -> Vec<DrawingToolType> {
83        let mut all_tools = Vec::new();
84        for category in self.get_all_categories() {
85            for (_, tools) in self.get_category_tools(&category) {
86                all_tools.extend(tools);
87            }
88        }
89        all_tools
90    }
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96
97    /// Toolbar categories include cursors, unknown names have no fallback, and
98    /// a case-insensitive search finds tools in their listed category.
99    #[test]
100    fn tools_are_found_by_category_and_search() {
101        let repo = InMemoryToolsRepository::new();
102        assert_eq!(repo.get_all_categories(), categories::CATEGORY_ORDER);
103        assert_eq!(categories::get_category_default_tool("Nonsense"), None);
104        assert!(DrawingToolType::by_category("Nonsense").is_empty());
105        let cases = [
106            (DrawingToolType::CrossCursor, "Cursors", "Cursors", "cross"),
107            (DrawingToolType::TrendLine, "Lines", "Lines", "trend"),
108            (
109                DrawingToolType::FibonacciRetracement,
110                "Fibonacci",
111                "Fibonacci",
112                "FIB",
113            ),
114            (
115                DrawingToolType::Table,
116                "Text/Annotations",
117                "Annotations",
118                "table",
119            ),
120            (
121                DrawingToolType::Image,
122                "Text/Annotations",
123                "Content",
124                "image",
125            ),
126            (
127                DrawingToolType::Tweet,
128                "Text/Annotations",
129                "Content",
130                "tweet",
131            ),
132            (DrawingToolType::Idea, "Text/Annotations", "Content", "idea"),
133            (
134                DrawingToolType::GhostFeed,
135                "Projection",
136                "Projection",
137                "ghost",
138            ),
139        ];
140        for (tool, category, command_category, query) in cases {
141            assert_eq!(
142                repo.get_tool_category(tool).as_deref(),
143                Some(category),
144                "{tool:?}"
145            );
146            assert!(
147                DrawingToolType::by_category(command_category).contains(&tool),
148                "{tool:?} missing from command search"
149            );
150            let listed = repo
151                .get_category_tools(category)
152                .iter()
153                .any(|(_, tools)| tools.contains(&tool));
154            assert!(listed, "{tool:?} is not listed under {category}");
155            assert!(
156                repo.search_tools(query).contains(&tool),
157                "search {query:?} misses {tool:?}"
158            );
159        }
160    }
161}