Skip to main content

agpu/widget/
tooltip.rs

1//! Tooltip widget that renders a positioned text popup.
2
3use crate::core::{Color, Position, Rect, TextStyle};
4use crate::ontology::{
5    AgentAction, AgentCapability, Discoverable, SemanticRole, UiNode, WidgetSchema,
6};
7use crate::paint::Painter;
8use crate::widget::Widget;
9
10/// A tooltip popup displaying helper text.
11pub struct Tooltip {
12    pub id: String,
13    pub text: String,
14    bg_color: Option<Color>,
15    fg_color: Option<Color>,
16    corner_radius: Option<f32>,
17    font_size: Option<f32>,
18}
19
20impl Tooltip {
21    #[must_use]
22    pub fn new(id: impl Into<String>, text: impl Into<String>) -> Self {
23        Self {
24            id: id.into(),
25            text: text.into(),
26            bg_color: None,
27            fg_color: None,
28            corner_radius: None,
29            font_size: None,
30        }
31    }
32
33    #[must_use]
34    pub fn bg(mut self, color: Color) -> Self {
35        self.bg_color = Some(color);
36        self
37    }
38
39    #[must_use]
40    pub fn fg(mut self, color: Color) -> Self {
41        self.fg_color = Some(color);
42        self
43    }
44
45    #[must_use]
46    pub fn rounded(mut self, radius: f32) -> Self {
47        self.corner_radius = Some(radius);
48        self
49    }
50
51    #[must_use]
52    pub fn text_size(mut self, size: f32) -> Self {
53        self.font_size = Some(size);
54        self
55    }
56}
57
58impl Widget for Tooltip {
59    fn draw(&self, painter: &mut dyn Painter, area: Rect) {
60        let bg = self.bg_color.unwrap_or(Color::rgba(0.12, 0.12, 0.16, 0.95));
61        let border = Color::rgba(0.35, 0.35, 0.45, 1.0);
62        let radius = self.corner_radius.unwrap_or(4.0);
63        painter.fill_rect(area, bg, radius);
64        painter.stroke_rect(area, border, 1.0, radius);
65
66        let style = TextStyle {
67            font_size: self.font_size.unwrap_or(12.0),
68            color: self.fg_color.unwrap_or(Color::rgba(0.9, 0.9, 0.9, 1.0)),
69            ..TextStyle::default()
70        };
71        let padding = 6.0;
72        painter.text(
73            Position::new(area.x + padding, area.y + padding),
74            &self.text,
75            &style,
76        );
77    }
78
79    fn ui_node(&self) -> UiNode {
80        UiNode::new("Tooltip", SemanticRole::Display).with_id(&self.id)
81    }
82}
83
84impl Discoverable for Tooltip {
85    fn schema(&self) -> WidgetSchema {
86        WidgetSchema::new(
87            "Tooltip",
88            "A tooltip popup showing help text",
89            SemanticRole::Display,
90        )
91    }
92
93    fn capabilities(&self) -> Vec<AgentCapability> {
94        vec![AgentCapability::HasTooltip]
95    }
96
97    fn actions(&self) -> Vec<AgentAction> {
98        vec![AgentAction::simple(
99            "set_text",
100            "Set tooltip text content",
101            true,
102        )]
103    }
104
105    fn semantic_role(&self) -> SemanticRole {
106        SemanticRole::Display
107    }
108
109    fn agent_state(&self) -> serde_json::Value {
110        serde_json::json!({ "text": self.text })
111    }
112
113    fn execute_action(
114        &mut self,
115        action: &str,
116        params: &serde_json::Value,
117    ) -> Result<serde_json::Value, String> {
118        match action {
119            "set_text" => {
120                if let Some(text) = params.get("text").and_then(|v| v.as_str()) {
121                    self.text = text.to_string();
122                    Ok(serde_json::json!({ "text": self.text }))
123                } else {
124                    Err("Missing 'text' parameter".into())
125                }
126            }
127            _ => Err(format!("Unknown action: {action}")),
128        }
129    }
130
131    fn agent_id(&self) -> Option<&str> {
132        Some(&self.id)
133    }
134}