1use std::vec::Vec;
24
25use azul_core::{
26 callbacks::{CoreCallbackData, Update},
27 dom::{Dom, IdOrClass, IdOrClass::Class, IdOrClassVec, TabIndex},
28 refany::RefAny,
29};
30use azul_css::dynamic_selector::{CssPropertyWithConditions, CssPropertyWithConditionsVec};
31use azul_css::{
32 props::{
33 basic::{color::ColorU, StyleFontSize},
34 layout::{LayoutDisplay, LayoutFlexDirection, LayoutAlignItems, LayoutAlignSelf, LayoutFlexGrow, LayoutJustifyContent, LayoutMinWidth, LayoutPaddingTop, LayoutPaddingBottom, LayoutPaddingLeft, LayoutPaddingRight},
35 property::{CssProperty, *},
36 style::{StyleBackgroundContent, StyleBackgroundContentVec, LayoutBorderTopWidth, LayoutBorderBottomWidth, LayoutBorderRightWidth, StyleBorderTopStyle, BorderStyle, StyleBorderBottomStyle, StyleBorderRightStyle, StyleBorderTopColor, StyleBorderBottomColor, StyleBorderRightColor, StyleCursor, StyleTextAlign, StyleUserSelect, StyleTextColor, LayoutBorderLeftWidth, StyleBorderLeftStyle, StyleBorderLeftColor, StyleBorderTopLeftRadius, StyleBorderBottomLeftRadius, StyleBorderTopRightRadius, StyleBorderBottomRightRadius},
37 },
38 impl_option_inner, AzString,
39};
40
41use crate::callbacks::{Callback, CallbackInfo};
42
43static PAGINATION_CLASS: &[IdOrClass] =
44 &[Class(AzString::from_const_str("__azul-native-pagination"))];
45static PAGINATION_PAGE_CLASS: &[IdOrClass] =
46 &[Class(AzString::from_const_str("__azul-native-pagination-page"))];
47static PAGINATION_NAV_CLASS: &[IdOrClass] =
48 &[Class(AzString::from_const_str("__azul-native-pagination-nav"))];
49
50const PREV_LABEL: AzString = AzString::from_const_str("Prev");
51const NEXT_LABEL: AzString = AzString::from_const_str("Next");
52
53pub type PaginationOnChangeCallbackType =
55 extern "C" fn(RefAny, CallbackInfo, PaginationState) -> Update;
56impl_widget_callback!(
57 PaginationOnChange,
58 OptionPaginationOnChange,
59 PaginationOnChangeCallback,
60 PaginationOnChangeCallbackType
61);
62
63azul_core::impl_managed_callback! {
64 wrapper: PaginationOnChangeCallback,
65 info_ty: CallbackInfo,
66 return_ty: Update,
67 default_ret: Update::DoNothing,
68 invoker_static: PAGINATION_ON_CHANGE_INVOKER,
69 invoker_ty: AzPaginationOnChangeCallbackInvoker,
70 thunk_fn: az_pagination_on_change_callback_thunk,
71 setter_fn: AzApp_setPaginationOnChangeCallbackInvoker,
72 from_handle_fn: AzPaginationOnChangeCallback_createFromHostHandle,
73 extra_args: [ state: PaginationState ],
74}
75
76#[derive(Debug, Clone, PartialEq, Eq)]
78#[repr(C)]
79pub struct Pagination {
80 pub pagination_state: PaginationStateWrapper,
81 pub container_style: CssPropertyWithConditionsVec,
83}
84
85#[derive(Debug, Default, Clone, PartialEq, Eq)]
86#[repr(C)]
87pub struct PaginationStateWrapper {
88 pub inner: PaginationState,
90 pub on_change: OptionPaginationOnChange,
92}
93
94#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)]
96#[repr(C)]
97pub struct PaginationState {
98 pub current_page: usize,
100 pub total_pages: usize,
102}
103
104const PAGE_BORDER_COLOR: ColorU = ColorU { r: 206, g: 212, b: 218, a: 255 };
107const ACCENT_BG_COLOR: ColorU = ColorU { r: 13, g: 110, b: 253, a: 255 };
109const NEUTRAL_BG_COLOR: ColorU = ColorU { r: 255, g: 255, b: 255, a: 255 };
111const ACTIVE_TEXT: ColorU = ColorU { r: 255, g: 255, b: 255, a: 255 };
113const NEUTRAL_TEXT: ColorU = ColorU { r: 33, g: 37, b: 41, a: 255 };
115const DISABLED_TEXT: ColorU = ColorU { r: 173, g: 181, b: 189, a: 255 };
117
118const ACCENT_BG_ITEMS: &[StyleBackgroundContent] =
119 &[StyleBackgroundContent::Color(ACCENT_BG_COLOR)];
120const ACCENT_BG: StyleBackgroundContentVec =
121 StyleBackgroundContentVec::from_const_slice(ACCENT_BG_ITEMS);
122const NEUTRAL_BG_ITEMS: &[StyleBackgroundContent] =
123 &[StyleBackgroundContent::Color(NEUTRAL_BG_COLOR)];
124const NEUTRAL_BG: StyleBackgroundContentVec =
125 StyleBackgroundContentVec::from_const_slice(NEUTRAL_BG_ITEMS);
126
127const PAGE_RADIUS: isize = 6;
128
129static PAGINATION_CONTAINER_STYLE: &[CssPropertyWithConditions] = &[
131 CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::Flex)),
132 CssPropertyWithConditions::simple(CssProperty::const_flex_direction(LayoutFlexDirection::Row)),
133 CssPropertyWithConditions::simple(CssProperty::const_align_items(LayoutAlignItems::Center)),
134 CssPropertyWithConditions::simple(CssProperty::align_self(LayoutAlignSelf::Start)),
135 CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(0))),
136];
137
138#[allow(clippy::too_many_lines)] #[allow(clippy::fn_params_excessive_bools)] fn build_button_style(
145 active: bool,
146 disabled: bool,
147 is_first: bool,
148 is_last: bool,
149) -> CssPropertyWithConditionsVec {
150 let bg = if active { ACCENT_BG } else { NEUTRAL_BG };
151 let text = if active {
152 ACTIVE_TEXT
153 } else if disabled {
154 DISABLED_TEXT
155 } else {
156 NEUTRAL_TEXT
157 };
158
159 let mut v: Vec<CssPropertyWithConditions> = vec![
160 CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::Flex)),
161 CssPropertyWithConditions::simple(CssProperty::const_flex_direction(
162 LayoutFlexDirection::Row,
163 )),
164 CssPropertyWithConditions::simple(CssProperty::const_justify_content(
165 LayoutJustifyContent::Center,
166 )),
167 CssPropertyWithConditions::simple(CssProperty::const_align_items(LayoutAlignItems::Center)),
168 CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(
169 0,
170 ))),
171 CssPropertyWithConditions::simple(CssProperty::const_min_width(LayoutMinWidth::const_px(
173 36,
174 ))),
175 CssPropertyWithConditions::simple(CssProperty::const_padding_top(LayoutPaddingTop::const_px(
177 6,
178 ))),
179 CssPropertyWithConditions::simple(CssProperty::const_padding_bottom(
180 LayoutPaddingBottom::const_px(6),
181 )),
182 CssPropertyWithConditions::simple(CssProperty::const_padding_left(
183 LayoutPaddingLeft::const_px(12),
184 )),
185 CssPropertyWithConditions::simple(CssProperty::const_padding_right(
186 LayoutPaddingRight::const_px(12),
187 )),
188 CssPropertyWithConditions::simple(CssProperty::const_border_top_width(
191 LayoutBorderTopWidth::const_px(1),
192 )),
193 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_width(
194 LayoutBorderBottomWidth::const_px(1),
195 )),
196 CssPropertyWithConditions::simple(CssProperty::const_border_right_width(
197 LayoutBorderRightWidth::const_px(1),
198 )),
199 CssPropertyWithConditions::simple(CssProperty::const_border_top_style(StyleBorderTopStyle {
200 inner: BorderStyle::Solid,
201 })),
202 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_style(
203 StyleBorderBottomStyle {
204 inner: BorderStyle::Solid,
205 },
206 )),
207 CssPropertyWithConditions::simple(CssProperty::const_border_right_style(
208 StyleBorderRightStyle {
209 inner: BorderStyle::Solid,
210 },
211 )),
212 CssPropertyWithConditions::simple(CssProperty::const_border_top_color(StyleBorderTopColor {
213 inner: PAGE_BORDER_COLOR,
214 })),
215 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_color(
216 StyleBorderBottomColor {
217 inner: PAGE_BORDER_COLOR,
218 },
219 )),
220 CssPropertyWithConditions::simple(CssProperty::const_border_right_color(
221 StyleBorderRightColor {
222 inner: PAGE_BORDER_COLOR,
223 },
224 )),
225 CssPropertyWithConditions::simple(CssProperty::const_cursor(StyleCursor::Pointer)),
226 CssPropertyWithConditions::simple(CssProperty::const_font_size(StyleFontSize::const_px(13))),
227 CssPropertyWithConditions::simple(CssProperty::const_text_align(StyleTextAlign::Center)),
228 CssPropertyWithConditions::simple(CssProperty::user_select(StyleUserSelect::None)),
229 CssPropertyWithConditions::simple(CssProperty::const_background_content(bg)),
230 CssPropertyWithConditions::simple(CssProperty::const_text_color(StyleTextColor {
231 inner: text,
232 })),
233 ];
234
235 if is_first {
236 v.push(CssPropertyWithConditions::simple(
237 CssProperty::const_border_left_width(LayoutBorderLeftWidth::const_px(1)),
238 ));
239 v.push(CssPropertyWithConditions::simple(
240 CssProperty::const_border_left_style(StyleBorderLeftStyle {
241 inner: BorderStyle::Solid,
242 }),
243 ));
244 v.push(CssPropertyWithConditions::simple(
245 CssProperty::const_border_left_color(StyleBorderLeftColor {
246 inner: PAGE_BORDER_COLOR,
247 }),
248 ));
249 v.push(CssPropertyWithConditions::simple(
250 CssProperty::const_border_top_left_radius(StyleBorderTopLeftRadius::const_px(
251 PAGE_RADIUS,
252 )),
253 ));
254 v.push(CssPropertyWithConditions::simple(
255 CssProperty::const_border_bottom_left_radius(StyleBorderBottomLeftRadius::const_px(
256 PAGE_RADIUS,
257 )),
258 ));
259 }
260 if is_last {
261 v.push(CssPropertyWithConditions::simple(
262 CssProperty::const_border_top_right_radius(StyleBorderTopRightRadius::const_px(
263 PAGE_RADIUS,
264 )),
265 ));
266 v.push(CssPropertyWithConditions::simple(
267 CssProperty::const_border_bottom_right_radius(StyleBorderBottomRightRadius::const_px(
268 PAGE_RADIUS,
269 )),
270 ));
271 }
272
273 CssPropertyWithConditionsVec::from_vec(v)
274}
275
276impl Pagination {
277 #[must_use] pub fn create(current_page: usize, total_pages: usize) -> Self {
280 let total_pages = total_pages.max(1);
281 let current_page = current_page.clamp(1, total_pages);
282 Self {
283 pagination_state: PaginationStateWrapper {
284 inner: PaginationState {
285 current_page,
286 total_pages,
287 },
288 ..Default::default()
289 },
290 container_style: CssPropertyWithConditionsVec::from_const_slice(
291 PAGINATION_CONTAINER_STYLE,
292 ),
293 }
294 }
295
296 #[inline]
298 pub fn set_current_page(&mut self, current_page: usize) {
299 let total = self.pagination_state.inner.total_pages.max(1);
300 self.pagination_state.inner.current_page = current_page.clamp(1, total);
301 }
302
303 #[inline]
305 #[must_use] pub fn with_current_page(mut self, current_page: usize) -> Self {
306 self.set_current_page(current_page);
307 self
308 }
309
310 #[inline]
311 #[must_use] pub fn swap_with_default(&mut self) -> Self {
312 let mut s = Self::create(1, 1);
313 core::mem::swap(&mut s, self);
314 s
315 }
316
317 #[inline]
318 pub fn set_on_change<C: Into<PaginationOnChangeCallback>>(
319 &mut self,
320 data: RefAny,
321 on_change: C,
322 ) {
323 self.pagination_state.on_change = Some(PaginationOnChange {
324 callback: on_change.into(),
325 refany: data,
326 })
327 .into();
328 }
329
330 #[inline]
331 #[must_use] pub fn with_on_change<C: Into<PaginationOnChangeCallback>>(
332 mut self,
333 data: RefAny,
334 on_change: C,
335 ) -> Self {
336 self.set_on_change(data, on_change);
337 self
338 }
339
340 #[must_use] pub fn dom(self) -> Dom {
341 use azul_core::{
342 callbacks::CoreCallback,
343 dom::{EventFilter, HoverEventFilter},
344 refany::OptionRefAny,
345 };
346
347 let current = self.pagination_state.inner.current_page;
348 let total = self.pagination_state.inner.total_pages;
349
350 let state = RefAny::new(self.pagination_state);
353
354 let make_button =
355 |label: AzString, class: &'static [IdOrClass], style: CssPropertyWithConditionsVec| {
356 Dom::create_text(label)
357 .with_ids_and_classes(IdOrClassVec::from_const_slice(class))
358 .with_css_props(style)
359 .with_callbacks(
360 vec![CoreCallbackData {
361 event: EventFilter::Hover(HoverEventFilter::MouseUp),
362 callback: CoreCallback {
363 cb: on_page_click as usize,
364 ctx: OptionRefAny::None,
365 },
366 refany: state.clone(),
367 }]
368 .into(),
369 )
370 .with_tab_index(TabIndex::Auto)
371 };
372
373 let mut children: Vec<Dom> = Vec::with_capacity(total.saturating_add(2));
374
375 children.push(make_button(
377 PREV_LABEL,
378 PAGINATION_NAV_CLASS,
379 build_button_style(false, current <= 1, true, false),
380 ));
381
382 for page in 1..=total {
384 children.push(make_button(
385 AzString::from(format!("{page}").as_str()),
386 PAGINATION_PAGE_CLASS,
387 build_button_style(page == current, false, false, false),
388 ));
389 }
390
391 children.push(make_button(
393 NEXT_LABEL,
394 PAGINATION_NAV_CLASS,
395 build_button_style(false, current >= total, false, true),
396 ));
397
398 Dom::create_div()
399 .with_ids_and_classes(IdOrClassVec::from_const_slice(PAGINATION_CLASS))
400 .with_css_props(self.container_style)
401 .with_children(children.into())
402 }
403}
404
405impl Default for Pagination {
406 fn default() -> Self {
407 Self::create(1, 1)
408 }
409}
410
411extern "C" fn on_page_click(mut data: RefAny, mut info: CallbackInfo) -> Update {
416 use azul_core::dom::DomNodeId;
417
418 let clicked = info.get_hit_node();
419 let Some(parent) = info.get_parent(clicked) else {
420 return Update::DoNothing;
421 };
422
423 let mut buttons: Vec<DomNodeId> = Vec::new();
425 let mut cur = info.get_first_child(parent);
426 while let Some(node) = cur {
427 buttons.push(node);
428 cur = info.get_next_sibling(node);
429 }
430 let n = buttons.len();
431 if n < 2 {
432 return Update::DoNothing;
433 }
434 let total = n - 2;
436
437 let Some(pos) = buttons.iter().position(|b| *b == clicked) else {
438 return Update::DoNothing;
439 };
440
441 let current = {
442 let Some(pg) = data.downcast_ref::<PaginationStateWrapper>() else {
443 return Update::DoNothing;
444 };
445 pg.inner.current_page
446 };
447
448 let new_page = if pos == 0 {
449 if current > 1 {
451 current - 1
452 } else {
453 current
454 }
455 } else if pos == n - 1 {
456 if current < total {
458 current + 1
459 } else {
460 current
461 }
462 } else {
463 pos
465 };
466
467 if new_page == current {
468 return Update::DoNothing;
470 }
471
472 let result = {
473 let Some(mut pg) = data.downcast_mut::<PaginationStateWrapper>() else {
474 return Update::DoNothing;
475 };
476 pg.inner.current_page = new_page;
477 let inner = pg.inner;
478 let pg = &mut *pg;
479 match pg.on_change.as_mut() {
480 Some(PaginationOnChange { callback, refany }) => {
481 (callback.cb)(refany.clone(), info, inner)
482 }
483 None => Update::DoNothing,
484 }
485 };
486
487 for (i, node) in buttons.iter().enumerate() {
490 let (bg, text) = if i == 0 {
491 let disabled = new_page <= 1;
493 (NEUTRAL_BG, if disabled { DISABLED_TEXT } else { NEUTRAL_TEXT })
494 } else if i == n - 1 {
495 let disabled = new_page >= total;
497 (NEUTRAL_BG, if disabled { DISABLED_TEXT } else { NEUTRAL_TEXT })
498 } else if i == new_page {
499 (ACCENT_BG, ACTIVE_TEXT)
500 } else {
501 (NEUTRAL_BG, NEUTRAL_TEXT)
502 };
503 info.set_css_property(*node, CssProperty::const_background_content(bg));
504 info.set_css_property(*node, CssProperty::const_text_color(StyleTextColor { inner: text }));
505 }
506
507 result
508}
509
510impl From<Pagination> for Dom {
511 fn from(p: Pagination) -> Self {
512 p.dom()
513 }
514}