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