1use std::vec::Vec;
17
18use azul_core::{
19 callbacks::{CoreCallbackData, Update},
20 dom::{Dom, IdOrClass, IdOrClass::Class, IdOrClassVec, TabIndex},
21 refany::RefAny,
22};
23use azul_css::dynamic_selector::{CssPropertyWithConditions, CssPropertyWithConditionsVec};
24use azul_css::{
25 props::{
26 basic::{color::ColorU, StyleFontSize},
27 layout::{LayoutDisplay, LayoutFlexDirection, LayoutJustifyContent, LayoutAlignItems, LayoutFlexGrow, LayoutWidth, LayoutHeight, LayoutAlignSelf, LayoutMarginRight, LayoutMarginBottom, LayoutMarginLeft},
28 property::{CssProperty, *},
29 style::{StyleBackgroundContent, StyleBackgroundContentVec, LayoutBorderTopWidth, LayoutBorderBottomWidth, LayoutBorderLeftWidth, LayoutBorderRightWidth, StyleBorderTopStyle, BorderStyle, StyleBorderBottomStyle, StyleBorderLeftStyle, StyleBorderRightStyle, StyleBorderTopColor, StyleBorderBottomColor, StyleBorderLeftColor, StyleBorderRightColor, StyleBorderTopLeftRadius, StyleBorderTopRightRadius, StyleBorderBottomLeftRadius, StyleBorderBottomRightRadius, StyleOpacity, StyleCursor, StyleUserSelect},
30 },
31 impl_option_inner, AzString, StringVec,
32};
33
34use crate::callbacks::{Callback, CallbackInfo};
35
36static RADIO_GROUP_CLASS: &[IdOrClass] =
37 &[Class(AzString::from_const_str("__azul-native-radio-group"))];
38static RADIO_GROUP_ROW_CLASS: &[IdOrClass] = &[Class(AzString::from_const_str(
39 "__azul-native-radio-group-row",
40))];
41static RADIO_GROUP_CIRCLE_CLASS: &[IdOrClass] = &[Class(AzString::from_const_str(
42 "__azul-native-radio-group-circle",
43))];
44static RADIO_GROUP_DOT_CLASS: &[IdOrClass] = &[Class(AzString::from_const_str(
45 "__azul-native-radio-group-dot",
46))];
47static RADIO_GROUP_LABEL_CLASS: &[IdOrClass] = &[Class(AzString::from_const_str(
48 "__azul-native-radio-group-label",
49))];
50
51pub type RadioGroupOnChangeCallbackType =
53 extern "C" fn(RefAny, CallbackInfo, RadioGroupState) -> Update;
54impl_widget_callback!(
55 RadioGroupOnChange,
56 OptionRadioGroupOnChange,
57 RadioGroupOnChangeCallback,
58 RadioGroupOnChangeCallbackType
59);
60
61azul_core::impl_managed_callback! {
62 wrapper: RadioGroupOnChangeCallback,
63 info_ty: CallbackInfo,
64 return_ty: Update,
65 default_ret: Update::DoNothing,
66 invoker_static: RADIO_GROUP_ON_CHANGE_INVOKER,
67 invoker_ty: AzRadioGroupOnChangeCallbackInvoker,
68 thunk_fn: az_radio_group_on_change_callback_thunk,
69 setter_fn: AzApp_setRadioGroupOnChangeCallbackInvoker,
70 from_handle_fn: AzRadioGroupOnChangeCallback_createFromHostHandle,
71 extra_args: [ state: RadioGroupState ],
72}
73
74#[derive(Debug, Clone, PartialEq, Eq)]
76#[repr(C)]
77pub struct RadioGroup {
78 pub radio_group_state: RadioGroupStateWrapper,
79 pub options: StringVec,
81 pub container_style: CssPropertyWithConditionsVec,
83}
84
85#[derive(Debug, Default, Clone, PartialEq, Eq)]
86#[repr(C)]
87pub struct RadioGroupStateWrapper {
88 pub inner: RadioGroupState,
90 pub horizontal: bool,
93 pub on_change: OptionRadioGroupOnChange,
95}
96
97#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)]
99#[repr(C)]
100pub struct RadioGroupState {
101 pub selected_index: usize,
103}
104
105const CIRCLE_SIZE: isize = 16;
107const CIRCLE_RADIUS: isize = 8;
108const CIRCLE_BORDER: isize = 1;
109const DOT_SIZE: isize = 8;
110const DOT_RADIUS: isize = 4;
111const ROW_GAP: isize = 6;
113const LABEL_GAP: isize = 8;
115
116const CIRCLE_BORDER_COLOR: ColorU = ColorU {
119 r: 155,
120 g: 155,
121 b: 155,
122 a: 255,
123};
124const DOT_COLOR: ColorU = ColorU {
126 r: 13,
127 g: 110,
128 b: 253,
129 a: 255,
130};
131
132const DOT_BG_ITEMS: &[StyleBackgroundContent] = &[StyleBackgroundContent::Color(DOT_COLOR)];
133const DOT_BG: StyleBackgroundContentVec = StyleBackgroundContentVec::from_const_slice(DOT_BG_ITEMS);
134
135static RADIO_GROUP_CIRCLE_STYLE: &[CssPropertyWithConditions] = &[
138 CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::Flex)),
139 CssPropertyWithConditions::simple(CssProperty::const_flex_direction(LayoutFlexDirection::Row)),
140 CssPropertyWithConditions::simple(CssProperty::const_justify_content(
141 LayoutJustifyContent::Center,
142 )),
143 CssPropertyWithConditions::simple(CssProperty::const_align_items(LayoutAlignItems::Center)),
144 CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(0))),
145 CssPropertyWithConditions::simple(CssProperty::const_width(LayoutWidth::const_px(CIRCLE_SIZE))),
146 CssPropertyWithConditions::simple(CssProperty::const_height(LayoutHeight::const_px(CIRCLE_SIZE))),
147 CssPropertyWithConditions::simple(CssProperty::const_border_top_width(
148 LayoutBorderTopWidth::const_px(CIRCLE_BORDER),
149 )),
150 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_width(
151 LayoutBorderBottomWidth::const_px(CIRCLE_BORDER),
152 )),
153 CssPropertyWithConditions::simple(CssProperty::const_border_left_width(
154 LayoutBorderLeftWidth::const_px(CIRCLE_BORDER),
155 )),
156 CssPropertyWithConditions::simple(CssProperty::const_border_right_width(
157 LayoutBorderRightWidth::const_px(CIRCLE_BORDER),
158 )),
159 CssPropertyWithConditions::simple(CssProperty::const_border_top_style(StyleBorderTopStyle {
160 inner: BorderStyle::Solid,
161 })),
162 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_style(
163 StyleBorderBottomStyle {
164 inner: BorderStyle::Solid,
165 },
166 )),
167 CssPropertyWithConditions::simple(CssProperty::const_border_left_style(StyleBorderLeftStyle {
168 inner: BorderStyle::Solid,
169 })),
170 CssPropertyWithConditions::simple(CssProperty::const_border_right_style(
171 StyleBorderRightStyle {
172 inner: BorderStyle::Solid,
173 },
174 )),
175 CssPropertyWithConditions::simple(CssProperty::const_border_top_color(StyleBorderTopColor {
176 inner: CIRCLE_BORDER_COLOR,
177 })),
178 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_color(
179 StyleBorderBottomColor {
180 inner: CIRCLE_BORDER_COLOR,
181 },
182 )),
183 CssPropertyWithConditions::simple(CssProperty::const_border_left_color(StyleBorderLeftColor {
184 inner: CIRCLE_BORDER_COLOR,
185 })),
186 CssPropertyWithConditions::simple(CssProperty::const_border_right_color(
187 StyleBorderRightColor {
188 inner: CIRCLE_BORDER_COLOR,
189 },
190 )),
191 CssPropertyWithConditions::simple(CssProperty::const_border_top_left_radius(
192 StyleBorderTopLeftRadius::const_px(CIRCLE_RADIUS),
193 )),
194 CssPropertyWithConditions::simple(CssProperty::const_border_top_right_radius(
195 StyleBorderTopRightRadius::const_px(CIRCLE_RADIUS),
196 )),
197 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_left_radius(
198 StyleBorderBottomLeftRadius::const_px(CIRCLE_RADIUS),
199 )),
200 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_right_radius(
201 StyleBorderBottomRightRadius::const_px(CIRCLE_RADIUS),
202 )),
203];
204
205static RADIO_GROUP_DOT_STYLE_SELECTED: &[CssPropertyWithConditions] = &[
207 CssPropertyWithConditions::simple(CssProperty::const_width(LayoutWidth::const_px(DOT_SIZE))),
208 CssPropertyWithConditions::simple(CssProperty::const_height(LayoutHeight::const_px(DOT_SIZE))),
209 CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(0))),
210 CssPropertyWithConditions::simple(CssProperty::const_background_content(DOT_BG)),
211 CssPropertyWithConditions::simple(CssProperty::const_border_top_left_radius(
212 StyleBorderTopLeftRadius::const_px(DOT_RADIUS),
213 )),
214 CssPropertyWithConditions::simple(CssProperty::const_border_top_right_radius(
215 StyleBorderTopRightRadius::const_px(DOT_RADIUS),
216 )),
217 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_left_radius(
218 StyleBorderBottomLeftRadius::const_px(DOT_RADIUS),
219 )),
220 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_right_radius(
221 StyleBorderBottomRightRadius::const_px(DOT_RADIUS),
222 )),
223 CssPropertyWithConditions::simple(CssProperty::const_opacity(StyleOpacity::const_new(100))),
224];
225
226static RADIO_GROUP_DOT_STYLE_UNSELECTED: &[CssPropertyWithConditions] = &[
228 CssPropertyWithConditions::simple(CssProperty::const_width(LayoutWidth::const_px(DOT_SIZE))),
229 CssPropertyWithConditions::simple(CssProperty::const_height(LayoutHeight::const_px(DOT_SIZE))),
230 CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(0))),
231 CssPropertyWithConditions::simple(CssProperty::const_background_content(DOT_BG)),
232 CssPropertyWithConditions::simple(CssProperty::const_border_top_left_radius(
233 StyleBorderTopLeftRadius::const_px(DOT_RADIUS),
234 )),
235 CssPropertyWithConditions::simple(CssProperty::const_border_top_right_radius(
236 StyleBorderTopRightRadius::const_px(DOT_RADIUS),
237 )),
238 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_left_radius(
239 StyleBorderBottomLeftRadius::const_px(DOT_RADIUS),
240 )),
241 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_right_radius(
242 StyleBorderBottomRightRadius::const_px(DOT_RADIUS),
243 )),
244 CssPropertyWithConditions::simple(CssProperty::const_opacity(StyleOpacity::const_new(0))),
245];
246
247fn build_container_style(horizontal: bool) -> CssPropertyWithConditionsVec {
250 let direction = if horizontal {
251 LayoutFlexDirection::Row
252 } else {
253 LayoutFlexDirection::Column
254 };
255 CssPropertyWithConditionsVec::from_vec(alloc::vec![
256 CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::Flex)),
257 CssPropertyWithConditions::simple(CssProperty::const_flex_direction(direction)),
258 CssPropertyWithConditions::simple(CssProperty::align_self(LayoutAlignSelf::Start)),
259 CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(
260 0,
261 ))),
262 ])
263}
264
265fn build_row_style(horizontal: bool) -> CssPropertyWithConditionsVec {
268 let mut v: Vec<CssPropertyWithConditions> = alloc::vec![
269 CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::Flex)),
270 CssPropertyWithConditions::simple(CssProperty::const_flex_direction(
271 LayoutFlexDirection::Row,
272 )),
273 CssPropertyWithConditions::simple(CssProperty::const_align_items(LayoutAlignItems::Center)),
274 CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(
275 0,
276 ))),
277 CssPropertyWithConditions::simple(CssProperty::const_cursor(StyleCursor::Pointer)),
278 CssPropertyWithConditions::simple(CssProperty::user_select(StyleUserSelect::None)),
279 ];
280 if horizontal {
281 v.push(CssPropertyWithConditions::simple(
282 CssProperty::const_margin_right(LayoutMarginRight::const_px(ROW_GAP * 2)),
283 ));
284 } else {
285 v.push(CssPropertyWithConditions::simple(
286 CssProperty::const_margin_bottom(LayoutMarginBottom::const_px(ROW_GAP)),
287 ));
288 }
289 CssPropertyWithConditionsVec::from_vec(v)
290}
291
292static RADIO_GROUP_LABEL_STYLE: &[CssPropertyWithConditions] = &[
294 CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(0))),
295 CssPropertyWithConditions::simple(CssProperty::const_font_size(StyleFontSize::const_px(13))),
296 CssPropertyWithConditions::simple(CssProperty::const_margin_left(LayoutMarginLeft::const_px(
297 LABEL_GAP,
298 ))),
299];
300
301impl RadioGroup {
302 #[must_use] pub fn create(options: StringVec) -> Self {
304 Self {
305 radio_group_state: RadioGroupStateWrapper {
306 inner: RadioGroupState { selected_index: 0 },
307 horizontal: false,
308 ..Default::default()
309 },
310 options,
311 container_style: build_container_style(false),
312 }
313 }
314
315 #[inline]
317 pub const fn set_selected_index(&mut self, selected_index: usize) {
318 self.radio_group_state.inner.selected_index = selected_index;
319 }
320
321 #[inline]
323 #[must_use] pub const fn with_selected_index(mut self, selected_index: usize) -> Self {
324 self.set_selected_index(selected_index);
325 self
326 }
327
328 #[inline]
330 pub fn set_horizontal(&mut self, horizontal: bool) {
331 self.radio_group_state.horizontal = horizontal;
332 self.container_style = build_container_style(horizontal);
333 }
334
335 #[inline]
337 #[must_use] pub fn with_horizontal(mut self, horizontal: bool) -> Self {
338 self.set_horizontal(horizontal);
339 self
340 }
341
342 #[inline]
343 #[must_use] pub fn swap_with_default(&mut self) -> Self {
344 let mut s = Self::create(StringVec::from_const_slice(&[]));
345 core::mem::swap(&mut s, self);
346 s
347 }
348
349 #[inline]
350 pub fn set_on_change<C: Into<RadioGroupOnChangeCallback>>(
351 &mut self,
352 data: RefAny,
353 on_change: C,
354 ) {
355 self.radio_group_state.on_change = Some(RadioGroupOnChange {
356 callback: on_change.into(),
357 refany: data,
358 })
359 .into();
360 }
361
362 #[inline]
363 #[must_use] pub fn with_on_change<C: Into<RadioGroupOnChangeCallback>>(
364 mut self,
365 data: RefAny,
366 on_change: C,
367 ) -> Self {
368 self.set_on_change(data, on_change);
369 self
370 }
371
372 #[must_use] pub fn dom(self) -> Dom {
373 use azul_core::{
374 callbacks::CoreCallback,
375 dom::{EventFilter, HoverEventFilter},
376 refany::OptionRefAny,
377 };
378
379 let selected = self.radio_group_state.inner.selected_index;
380 let horizontal = self.radio_group_state.horizontal;
381 let count = self.options.as_ref().len();
382
383 let row_style = build_row_style(horizontal);
384
385 let state = RefAny::new(self.radio_group_state);
388
389 let mut children: Vec<Dom> = Vec::with_capacity(count);
390 for (i, label) in self.options.as_ref().iter().enumerate() {
391 let dot_style = if i == selected {
392 CssPropertyWithConditionsVec::from_const_slice(RADIO_GROUP_DOT_STYLE_SELECTED)
393 } else {
394 CssPropertyWithConditionsVec::from_const_slice(RADIO_GROUP_DOT_STYLE_UNSELECTED)
395 };
396
397 let circle = Dom::create_div()
398 .with_ids_and_classes(IdOrClassVec::from_const_slice(RADIO_GROUP_CIRCLE_CLASS))
399 .with_css_props(CssPropertyWithConditionsVec::from_const_slice(
400 RADIO_GROUP_CIRCLE_STYLE,
401 ))
402 .with_children(
403 vec![Dom::create_div()
404 .with_ids_and_classes(IdOrClassVec::from_const_slice(
405 RADIO_GROUP_DOT_CLASS,
406 ))
407 .with_css_props(dot_style)]
408 .into(),
409 );
410
411 let label_node = Dom::create_text(label.clone())
412 .with_ids_and_classes(IdOrClassVec::from_const_slice(RADIO_GROUP_LABEL_CLASS))
413 .with_css_props(CssPropertyWithConditionsVec::from_const_slice(
414 RADIO_GROUP_LABEL_STYLE,
415 ));
416
417 children.push(
418 Dom::create_div()
419 .with_ids_and_classes(IdOrClassVec::from_const_slice(RADIO_GROUP_ROW_CLASS))
420 .with_css_props(row_style.clone())
421 .with_callbacks(
422 vec![CoreCallbackData {
423 event: EventFilter::Hover(HoverEventFilter::MouseUp),
424 callback: CoreCallback {
425 cb: on_radio_row_click as usize,
426 ctx: OptionRefAny::None,
427 },
428 refany: state.clone(),
429 }]
430 .into(),
431 )
432 .with_tab_index(TabIndex::Auto)
433 .with_children(vec![circle, label_node].into()),
434 );
435 }
436
437 Dom::create_div()
438 .with_ids_and_classes(IdOrClassVec::from_const_slice(RADIO_GROUP_CLASS))
439 .with_css_props(self.container_style)
440 .with_children(children.into())
441 }
442}
443
444impl Default for RadioGroup {
445 fn default() -> Self {
446 Self::create(StringVec::from_const_slice(&[]))
447 }
448}
449
450extern "C" fn on_radio_row_click(mut data: RefAny, mut info: CallbackInfo) -> Update {
456 use azul_core::dom::DomNodeId;
457
458 let clicked = info.get_hit_node();
459 let Some(parent) = info.get_parent(clicked) else {
460 return Update::DoNothing;
461 };
462
463 let mut rows: Vec<DomNodeId> = Vec::new();
465 let mut cur = info.get_first_child(parent);
466 while let Some(node) = cur {
467 rows.push(node);
468 cur = info.get_next_sibling(node);
469 }
470
471 let Some(selected) = rows.iter().position(|n| *n == clicked) else {
472 return Update::DoNothing;
473 };
474
475 let result = {
476 let Some(mut rg) = data.downcast_mut::<RadioGroupStateWrapper>() else {
477 return Update::DoNothing;
478 };
479 rg.inner.selected_index = selected;
480 let inner = rg.inner;
481 let rg = &mut *rg;
482 match rg.on_change.as_mut() {
483 Some(RadioGroupOnChange { callback, refany }) => {
484 (callback.cb)(refany.clone(), info, inner)
485 }
486 None => Update::DoNothing,
487 }
488 };
489
490 for (i, row) in rows.iter().enumerate() {
494 let Some(circle) = info.get_first_child(*row) else {
495 continue;
496 };
497 let Some(dot) = info.get_first_child(circle) else {
498 continue;
499 };
500 let opacity = if i == selected { 100 } else { 0 };
501 info.set_css_property(dot, CssProperty::const_opacity(StyleOpacity::const_new(opacity)));
502 }
503
504 result
505}
506
507impl From<RadioGroup> for Dom {
508 fn from(r: RadioGroup) -> Self {
509 r.dom()
510 }
511}