Rust · 9115 bytes Raw Blame History
1 //! Session selector dropdown widget
2 //!
3 //! Allows users to choose which desktop session to start.
4
5 use crate::icons;
6 use crate::render::rounded_rectangle;
7 use crate::theme::Theme;
8 use anyhow::Result;
9 use cairo::Context;
10 use gardm_ipc::SessionInfo;
11 use pango::{FontDescription, Layout};
12
13 /// Session selector dropdown
14 pub struct SessionSelector {
15 sessions: Vec<SessionInfo>,
16 selected_index: usize,
17 expanded: bool,
18 hovered_index: Option<usize>,
19
20 // Layout
21 x: f64,
22 y: f64,
23 width: f64,
24 item_height: f64,
25 }
26
27 impl SessionSelector {
28 /// Create a new session selector
29 pub fn new(sessions: Vec<SessionInfo>, x: f64, y: f64, width: f64) -> Self {
30 Self {
31 sessions,
32 selected_index: 0,
33 expanded: false,
34 hovered_index: None,
35 x,
36 y,
37 width,
38 item_height: 40.0,
39 }
40 }
41
42 /// Get the currently selected session
43 pub fn selected(&self) -> Option<&SessionInfo> {
44 self.sessions.get(self.selected_index)
45 }
46
47 /// Get the exec command for the selected session
48 pub fn selected_exec(&self) -> Option<&str> {
49 self.selected().map(|s| s.exec.as_str())
50 }
51
52 /// Get the session type for the selected session ("x11" or "wayland")
53 pub fn selected_type(&self) -> Option<&str> {
54 self.selected().map(|s| s.session_type.as_str())
55 }
56
57 /// Check if dropdown is expanded
58 pub fn is_expanded(&self) -> bool {
59 self.expanded
60 }
61
62 /// Toggle dropdown expanded state
63 pub fn toggle(&mut self) {
64 self.expanded = !self.expanded;
65 if !self.expanded {
66 self.hovered_index = None;
67 }
68 }
69
70 /// Close the dropdown
71 pub fn close(&mut self) {
72 self.expanded = false;
73 self.hovered_index = None;
74 }
75
76 /// Render the session selector
77 pub fn render(&self, ctx: &Context, pango_ctx: &pango::Context, theme: &Theme) -> Result<()> {
78 // Main button (always visible)
79 self.render_button(ctx, pango_ctx, theme)?;
80
81 // Dropdown list (when expanded)
82 if self.expanded && !self.sessions.is_empty() {
83 self.render_dropdown(ctx, pango_ctx, theme)?;
84 }
85
86 Ok(())
87 }
88
89 fn render_button(&self, ctx: &Context, pango_ctx: &pango::Context, theme: &Theme) -> Result<()> {
90 // Background
91 let bg = &theme.input_background;
92 ctx.set_source_rgba(bg.r, bg.g, bg.b, 0.9);
93 rounded_rectangle(ctx, self.x, self.y, self.width, self.item_height, 8.0);
94 ctx.fill()?;
95
96 // Border
97 ctx.set_source_rgba(0.4, 0.4, 0.4, 0.8);
98 rounded_rectangle(ctx, self.x, self.y, self.width, self.item_height, 8.0);
99 ctx.set_line_width(1.0);
100 ctx.stroke()?;
101
102 // Session name
103 let name = self
104 .selected()
105 .map(|s| s.name.as_str())
106 .unwrap_or("No sessions");
107
108 let mut font = FontDescription::new();
109 font.set_family(&theme.font_family);
110 font.set_size(13 * pango::SCALE);
111
112 let layout = Layout::new(pango_ctx);
113 layout.set_font_description(Some(&font));
114 layout.set_text(name);
115
116 let tc = &theme.text_primary;
117 ctx.set_source_rgb(tc.r, tc.g, tc.b);
118 ctx.move_to(self.x + 12.0, self.y + (self.item_height - 16.0) / 2.0);
119 pangocairo::functions::show_layout(ctx, &layout);
120
121 // Chevron icon
122 let chevron_size = 16.0;
123 let chevron_x = self.x + self.width - chevron_size - 12.0;
124 let chevron_y = self.y + (self.item_height - chevron_size) / 2.0;
125 let sc = &theme.text_secondary;
126 ctx.set_source_rgba(sc.r, sc.g, sc.b, sc.a);
127 icons::draw_chevron_down(ctx, chevron_x, chevron_y, chevron_size);
128
129 Ok(())
130 }
131
132 fn render_dropdown(&self, ctx: &Context, pango_ctx: &pango::Context, theme: &Theme) -> Result<()> {
133 let dropdown_height = self.sessions.len() as f64 * self.item_height;
134 let dropdown_y = self.y - dropdown_height - 4.0; // Above the button
135
136 // Dropdown background
137 let bg = &theme.panel_background;
138 ctx.set_source_rgba(bg.r * 0.8, bg.g * 0.8, bg.b * 0.8, 0.95);
139 rounded_rectangle(ctx, self.x, dropdown_y, self.width, dropdown_height, 8.0);
140 ctx.fill()?;
141
142 // Border
143 ctx.set_source_rgba(0.3, 0.3, 0.3, 0.8);
144 rounded_rectangle(ctx, self.x, dropdown_y, self.width, dropdown_height, 8.0);
145 ctx.set_line_width(1.0);
146 ctx.stroke()?;
147
148 // Items
149 let mut font = FontDescription::new();
150 font.set_family(&theme.font_family);
151 font.set_size(13 * pango::SCALE);
152
153 for (i, session) in self.sessions.iter().enumerate() {
154 let item_y = dropdown_y + i as f64 * self.item_height;
155 let is_selected = i == self.selected_index;
156 let is_hovered = self.hovered_index == Some(i);
157
158 // Item background on hover
159 if is_hovered {
160 let ac = &theme.accent;
161 ctx.set_source_rgba(ac.r, ac.g, ac.b, 0.5);
162 if i == 0 {
163 // First item - round top corners
164 rounded_rectangle(ctx, self.x + 2.0, item_y + 2.0, self.width - 4.0, self.item_height - 2.0, 6.0);
165 } else if i == self.sessions.len() - 1 {
166 // Last item - round bottom corners
167 rounded_rectangle(ctx, self.x + 2.0, item_y, self.width - 4.0, self.item_height - 2.0, 6.0);
168 } else {
169 ctx.rectangle(self.x + 2.0, item_y, self.width - 4.0, self.item_height);
170 }
171 ctx.fill()?;
172 }
173
174 // Checkmark for selected item
175 if is_selected {
176 ctx.set_source_rgba(0.4, 0.8, 0.4, 1.0);
177 icons::draw_checkmark(ctx, self.x + 8.0, item_y + 8.0, 24.0);
178 }
179
180 // Session name
181 let layout = Layout::new(pango_ctx);
182 layout.set_font_description(Some(&font));
183 layout.set_text(&session.name);
184
185 let tc = &theme.text_primary;
186 if is_selected {
187 ctx.set_source_rgb(tc.r, tc.g, tc.b);
188 } else {
189 ctx.set_source_rgba(tc.r, tc.g, tc.b, 0.9);
190 }
191 ctx.move_to(self.x + 36.0, item_y + (self.item_height - 16.0) / 2.0);
192 pangocairo::functions::show_layout(ctx, &layout);
193 }
194
195 Ok(())
196 }
197
198 /// Update hover state based on mouse position
199 pub fn update_hover(&mut self, mouse_x: f64, mouse_y: f64) -> bool {
200 if !self.expanded {
201 return false;
202 }
203
204 let dropdown_height = self.sessions.len() as f64 * self.item_height;
205 let dropdown_y = self.y - dropdown_height - 4.0;
206
207 let old_hover = self.hovered_index;
208
209 // Check if mouse is in dropdown area
210 if mouse_x >= self.x
211 && mouse_x <= self.x + self.width
212 && mouse_y >= dropdown_y
213 && mouse_y <= dropdown_y + dropdown_height
214 {
215 let relative_y = mouse_y - dropdown_y;
216 self.hovered_index = Some((relative_y / self.item_height) as usize);
217 } else {
218 self.hovered_index = None;
219 }
220
221 self.hovered_index != old_hover
222 }
223
224 /// Check if click is on the main button
225 pub fn button_contains(&self, x: f64, y: f64) -> bool {
226 x >= self.x && x <= self.x + self.width && y >= self.y && y <= self.y + self.item_height
227 }
228
229 /// Check if click is in the dropdown and handle selection
230 /// Returns true if a selection was made
231 pub fn handle_dropdown_click(&mut self, click_x: f64, click_y: f64) -> bool {
232 if !self.expanded {
233 return false;
234 }
235
236 let dropdown_height = self.sessions.len() as f64 * self.item_height;
237 let dropdown_y = self.y - dropdown_height - 4.0;
238
239 if click_x >= self.x
240 && click_x <= self.x + self.width
241 && click_y >= dropdown_y
242 && click_y <= dropdown_y + dropdown_height
243 {
244 let relative_y = click_y - dropdown_y;
245 let index = (relative_y / self.item_height) as usize;
246 if index < self.sessions.len() {
247 self.selected_index = index;
248 self.close();
249 return true;
250 }
251 }
252
253 false
254 }
255
256 /// Check if a point is within the selector bounds (button or dropdown)
257 pub fn contains(&self, x: f64, y: f64) -> bool {
258 // Check main button
259 if self.button_contains(x, y) {
260 return true;
261 }
262
263 // Check dropdown if expanded
264 if self.expanded {
265 let dropdown_height = self.sessions.len() as f64 * self.item_height;
266 let dropdown_y = self.y - dropdown_height - 4.0;
267
268 if x >= self.x
269 && x <= self.x + self.width
270 && y >= dropdown_y
271 && y <= dropdown_y + dropdown_height
272 {
273 return true;
274 }
275 }
276
277 false
278 }
279 }
280