armas_basic/components/
input_group.rs1use crate::ext::ArmasContextExt;
19use egui::{vec2, Id, Sense, Stroke, Ui};
20
21const HEIGHT: f32 = 40.0;
23const CORNER_RADIUS: f32 = 6.0;
24const ADDON_PADDING: f32 = 10.0;
25const INPUT_PADDING: f32 = 12.0;
26type AddonFn = Box<dyn FnOnce(&mut Ui)>;
30
31pub struct InputGroup {
33 id: Id,
34 width: Option<f32>,
35 placeholder: String,
36 leading: Option<AddonFn>,
37 trailing: Option<AddonFn>,
38}
39
40pub struct InputGroupResponse {
42 pub response: egui::Response,
44 pub changed: bool,
46}
47
48impl InputGroup {
49 pub fn new(id: impl Into<Id>) -> Self {
51 Self {
52 id: id.into(),
53 width: None,
54 placeholder: String::new(),
55 leading: None,
56 trailing: None,
57 }
58 }
59
60 #[must_use]
62 pub const fn width(mut self, w: f32) -> Self {
63 self.width = Some(w);
64 self
65 }
66
67 #[must_use]
69 pub fn placeholder(mut self, text: impl Into<String>) -> Self {
70 self.placeholder = text.into();
71 self
72 }
73
74 #[must_use]
76 pub fn leading(mut self, content: impl FnOnce(&mut Ui) + 'static) -> Self {
77 self.leading = Some(Box::new(content));
78 self
79 }
80
81 #[must_use]
83 pub fn trailing(mut self, content: impl FnOnce(&mut Ui) + 'static) -> Self {
84 self.trailing = Some(Box::new(content));
85 self
86 }
87
88 pub fn show(self, ui: &mut Ui, text: &mut String) -> InputGroupResponse {
90 let theme = ui.ctx().armas_theme();
91 let width = self
92 .width
93 .unwrap_or_else(|| ui.available_width().min(300.0));
94
95 let state_id = self.id.with("input_state");
97 let stored: Option<String> = ui.ctx().data_mut(|d| d.get_temp(state_id));
98 if let Some(stored) = stored {
99 *text = stored;
100 }
101
102 let (outer_rect, _) = ui.allocate_exact_size(vec2(width, HEIGHT), Sense::hover());
104
105 ui.painter()
107 .rect_filled(outer_rect, CORNER_RADIUS, theme.background());
108 ui.painter().rect_stroke(
109 outer_rect,
110 CORNER_RADIUS,
111 Stroke::new(1.0, theme.input()),
112 egui::epaint::StrokeKind::Inside,
113 );
114
115 let mut leading_width = 0.0;
117 let mut trailing_width = 0.0;
118
119 if let Some(leading) = self.leading {
121 let addon_rect = egui::Rect::from_min_size(
122 outer_rect.left_top(),
123 vec2(outer_rect.width() * 0.3, HEIGHT), );
125
126 let mut child_ui = ui.new_child(
127 egui::UiBuilder::new()
128 .max_rect(addon_rect)
129 .layout(egui::Layout::left_to_right(egui::Align::Center)),
130 );
131 child_ui.set_clip_rect(outer_rect);
132 child_ui.add_space(ADDON_PADDING);
133 child_ui.style_mut().visuals.override_text_color = Some(theme.muted_foreground());
134 leading(&mut child_ui);
135 child_ui.add_space(ADDON_PADDING);
136 leading_width = child_ui.min_rect().width();
137
138 let sep_x = outer_rect.left() + leading_width;
140 ui.painter().line_segment(
141 [
142 egui::Pos2::new(sep_x, outer_rect.top() + 1.0),
143 egui::Pos2::new(sep_x, outer_rect.bottom() - 1.0),
144 ],
145 Stroke::new(1.0, theme.border()),
146 );
147 }
148
149 if let Some(trailing) = self.trailing {
151 let addon_rect = egui::Rect::from_min_size(
153 egui::Pos2::new(
154 outer_rect.right() - outer_rect.width() * 0.3,
155 outer_rect.top(),
156 ),
157 vec2(outer_rect.width() * 0.3, HEIGHT),
158 );
159
160 let mut child_ui = ui.new_child(
161 egui::UiBuilder::new()
162 .max_rect(addon_rect)
163 .layout(egui::Layout::right_to_left(egui::Align::Center)),
164 );
165 child_ui.set_clip_rect(outer_rect);
166 child_ui.add_space(ADDON_PADDING);
167 child_ui.style_mut().visuals.override_text_color = Some(theme.muted_foreground());
168 trailing(&mut child_ui);
169 child_ui.add_space(ADDON_PADDING);
170 trailing_width = child_ui.min_rect().width();
171
172 let sep_x = outer_rect.right() - trailing_width;
174 ui.painter().line_segment(
175 [
176 egui::Pos2::new(sep_x, outer_rect.top() + 1.0),
177 egui::Pos2::new(sep_x, outer_rect.bottom() - 1.0),
178 ],
179 Stroke::new(1.0, theme.border()),
180 );
181 }
182
183 let input_left = outer_rect.left() + leading_width;
185 let input_right = outer_rect.right() - trailing_width;
186 let input_rect = egui::Rect::from_min_max(
187 egui::Pos2::new(input_left + INPUT_PADDING, outer_rect.top()),
188 egui::Pos2::new(input_right - INPUT_PADDING, outer_rect.bottom()),
189 );
190
191 let prev_text = text.clone();
192
193 let mut child_ui = ui.new_child(
194 egui::UiBuilder::new()
195 .id_salt(self.id.with("input_area"))
196 .max_rect(input_rect)
197 .layout(egui::Layout::left_to_right(egui::Align::Center)),
198 );
199 child_ui.set_clip_rect(egui::Rect::from_min_max(
200 egui::Pos2::new(input_left, outer_rect.top()),
201 egui::Pos2::new(input_right, outer_rect.bottom()),
202 ));
203
204 child_ui.style_mut().visuals.extreme_bg_color = theme.background();
206 child_ui.style_mut().visuals.widgets.inactive.bg_stroke = Stroke::NONE;
207 child_ui.style_mut().visuals.widgets.hovered.bg_stroke = Stroke::NONE;
208 child_ui.style_mut().visuals.widgets.active.bg_stroke = Stroke::NONE;
209 child_ui.style_mut().visuals.widgets.inactive.bg_fill = theme.background();
210 child_ui.style_mut().visuals.widgets.hovered.bg_fill = theme.background();
211 child_ui.style_mut().visuals.widgets.active.bg_fill = theme.background();
212 child_ui.style_mut().visuals.selection.bg_fill = theme.primary();
213
214 let te = egui::TextEdit::singleline(text)
215 .id(self.id.with("input"))
216 .font(egui::TextStyle::Body)
217 .text_color(theme.foreground())
218 .desired_width(input_rect.width())
219 .hint_text(
220 egui::RichText::new(&self.placeholder)
221 .size(theme.typography.base)
222 .color(theme.muted_foreground()),
223 )
224 .frame(false);
225
226 let te_response = child_ui.add(te);
227 let changed = *text != prev_text;
228
229 if te_response.has_focus() {
231 ui.painter().rect_stroke(
232 outer_rect,
233 CORNER_RADIUS,
234 Stroke::new(2.0, theme.ring()),
235 egui::epaint::StrokeKind::Inside,
236 );
237 }
238
239 ui.ctx().data_mut(|d| d.insert_temp(state_id, text.clone()));
241
242 InputGroupResponse {
243 response: te_response,
244 changed,
245 }
246 }
247}
248
249#[cfg(test)]
250mod tests {
251 use super::*;
252
253 #[test]
254 fn test_input_group_builder() {
255 let ig = InputGroup::new("test")
256 .width(400.0)
257 .placeholder("Search...");
258 assert_eq!(ig.width, Some(400.0));
259 assert_eq!(ig.placeholder, "Search...");
260 }
261}