use crate::input::core::coordinator::LayerId;
use crate::input::{InputCoordinator, Sense, WidgetKind};
use crate::render::{RenderContext, TextAlign, TextBaseline};
use crate::types::{Rect, WidgetId, WidgetState, CompositeId};
use crate::ui::widgets::atomic::button::render::{
draw_danger_button, draw_ghost_outline_button, draw_primary_button, DangerButtonView,
DangerVariant, GhostOutlineButtonView, PrimaryButtonView,
};
use crate::ui::widgets::atomic::button::theme::DefaultButtonTheme;
use crate::ui::widgets::atomic::close_button::render::{draw_close_button, CloseButtonView};
use crate::ui::widgets::atomic::close_button::settings::CloseButtonSettings;
use crate::ui::widgets::atomic::close_button::style::DefaultCloseButtonStyle;
use crate::ui::widgets::atomic::close_button::theme::DefaultCloseButtonTheme;
use crate::ui::widgets::atomic::close_button::types::CloseButtonRenderKind;
use crate::ui::widgets::atomic::drag_handle::render::draw_drag_handle;
use crate::ui::widgets::atomic::drag_handle::settings::DragHandleSettings;
use crate::ui::widgets::atomic::drag_handle::types::{DragHandleRenderKind, DragHandleView};
use crate::ui::widgets::atomic::tab::render::{
draw_modal_horizontal_tab, draw_modal_sidebar_tab, TabView,
};
use crate::ui::widgets::atomic::tab::style::{ModalHorizontalTabStyle, ModalSidebarTabStyle};
use crate::ui::widgets::atomic::tab::theme::DefaultTabTheme;
use crate::ui::widgets::atomic::tab::types::TabConfig;
use crate::ui::widgets::atomic::text::render::draw_text;
use crate::ui::widgets::atomic::text::settings::TextSettings;
use crate::ui::widgets::atomic::text::types::{TextOverflow, TextView};
use super::settings::ModalSettings;
use super::state::ModalState;
use super::style::BackgroundFill;
use super::types::{BackdropKind, FooterBtnStyle, ModalRenderKind, ModalView};
struct ModalLayout {
header: Rect,
close_btn: Rect,
drag_handle: Rect,
tab_strip: Rect,
sidebar: Rect,
footer: Rect,
wizard_nav: Rect,
}
pub fn register_input_coordinator_modal(
coord: &mut InputCoordinator,
id: impl Into<WidgetId>,
rect: Rect,
state: &mut ModalState,
_view: &ModalView<'_>,
settings: &ModalSettings,
kind: &ModalRenderKind,
layer: &LayerId,
) -> CompositeId {
let modal_id = coord.register_composite(id, WidgetKind::Modal, rect, Sense::CLICK, layer);
match kind {
ModalRenderKind::Custom(_) => {
return modal_id;
}
_ => {}
}
let frame = resolve_frame(rect, state, kind);
let layout = compute_layout(frame, state, _view, settings, kind);
let mid = &modal_id.0.0;
if layout.close_btn.width > 0.0 {
coord.register_child(
&modal_id,
format!("{}:close", mid),
WidgetKind::CloseButton,
layout.close_btn,
Sense::CLICK,
);
}
if layout.drag_handle.width > 0.0 && layout.drag_handle.height > 0.0 {
coord.register_child(
&modal_id,
format!("{}:drag", mid),
WidgetKind::DragHandle,
layout.drag_handle,
Sense::DRAG,
);
}
if !_view.tabs.is_empty() {
let tab_count = _view.tabs.len();
match kind {
ModalRenderKind::TopTabs if layout.tab_strip.height > 0.0 => {
let tab_w = layout.tab_strip.width / tab_count as f64;
for i in 0..tab_count {
let tab_rect = Rect::new(
layout.tab_strip.x + i as f64 * tab_w,
layout.tab_strip.y,
tab_w,
layout.tab_strip.height,
);
coord.register_child(
&modal_id,
format!("{}:tab:{}", mid, i),
WidgetKind::Button,
tab_rect,
Sense::CLICK | Sense::HOVER,
);
}
}
ModalRenderKind::SideTabs if layout.sidebar.width > 0.0 => {
let tab_h = layout.sidebar.height / tab_count as f64;
for i in 0..tab_count {
let tab_rect = Rect::new(
layout.sidebar.x,
layout.sidebar.y + i as f64 * tab_h,
layout.sidebar.width,
tab_h,
);
coord.register_child(
&modal_id,
format!("{}:tab:{}", mid, i),
WidgetKind::Button,
tab_rect,
Sense::CLICK | Sense::HOVER,
);
}
}
_ => {}
}
}
if layout.footer.height > 0.0 && !_view.footer_buttons.is_empty() {
let btn_w = 80.0_f64;
let btn_h = 28.0_f64;
let btn_gap = 8.0_f64;
let padding_right = 16.0_f64;
let total_btns = _view.footer_buttons.len();
let total_w = total_btns as f64 * btn_w
+ (total_btns.saturating_sub(1)) as f64 * btn_gap;
let start_x = layout.footer.x + layout.footer.width - total_w - padding_right;
let btn_y = layout.footer.y + (layout.footer.height - btn_h) / 2.0;
for (i, _btn) in _view.footer_buttons.iter().enumerate() {
let btn_x = start_x + i as f64 * (btn_w + btn_gap);
coord.register_child(
&modal_id,
format!("{}:footer:{}", mid, i),
WidgetKind::Button,
Rect::new(btn_x, btn_y, btn_w, btn_h),
Sense::CLICK | Sense::HOVER,
);
}
}
register_body_overflow(coord, &modal_id, frame, _view, settings, kind, state);
if _view.resizable {
register_resize_handles(coord, &modal_id, frame);
}
if matches!(kind, ModalRenderKind::Wizard) && !_view.wizard_pages.is_empty() {
let style = settings.style.as_ref();
let btn_w = 72.0_f64;
let btn_h = 28.0_f64;
let padding_x = style.padding();
let nav = layout.wizard_nav;
let btn_y = nav.y + (nav.height - btn_h) / 2.0;
if state.current_page > 0 {
coord.register_child(
&modal_id,
format!("{}:wizard:back", mid),
WidgetKind::Button,
Rect::new(nav.x + padding_x, btn_y, btn_w, btn_h),
Sense::CLICK,
);
}
coord.register_child(
&modal_id,
format!("{}:wizard:next", mid),
WidgetKind::Button,
Rect::new(nav.x + nav.width - padding_x - btn_w, btn_y, btn_w, btn_h),
Sense::CLICK,
);
}
modal_id
}
pub fn register_context_manager_modal(
ctx_mgr: &mut crate::app_context::ContextManager,
render: &mut dyn RenderContext,
id: impl Into<WidgetId>,
rect: Rect,
state: &mut ModalState,
view: &mut ModalView<'_>,
settings: &ModalSettings,
kind: &ModalRenderKind,
layer: &LayerId,
) {
let coord = &mut ctx_mgr.input;
let modal_id = register_input_coordinator_modal(coord, id, rect, state, view, settings, kind, layer);
let _ = (coord, modal_id);
draw_modal(render, rect, state, view, settings, kind);
}
pub fn draw_modal(
ctx: &mut dyn RenderContext,
rect: Rect,
state: &ModalState,
view: &mut ModalView<'_>,
settings: &ModalSettings,
kind: &ModalRenderKind,
) {
match kind {
ModalRenderKind::Custom(f) => {
f(ctx, rect, view, settings);
return;
}
_ => {}
}
let theme = settings.theme.as_ref();
let style = settings.style.as_ref();
let frame = resolve_frame(rect, state, kind);
let layout = compute_layout(frame, state, view, settings, kind);
match view.backdrop {
BackdropKind::None => {}
BackdropKind::Dim => {
ctx.set_fill_color(theme.backdrop_dim());
ctx.fill_rect(0.0, 0.0, 99_999.0, 99_999.0);
}
BackdropKind::FullBlock => {
ctx.set_fill_color(theme.backdrop_full());
ctx.fill_rect(0.0, 0.0, 99_999.0, 99_999.0);
}
}
let offset = style.shadow_offset();
ctx.set_fill_color(theme.shadow());
ctx.fill_rounded_rect(
frame.x + offset,
frame.y + offset,
frame.width,
frame.height,
style.radius(),
);
match style.background_fill() {
BackgroundFill::Solid => {
ctx.set_fill_color(theme.bg());
ctx.fill_rounded_rect(frame.x, frame.y, frame.width, frame.height, style.radius());
}
BackgroundFill::Glass { blur_radius: _ } => {
ctx.draw_blur_background(frame.x, frame.y, frame.width, frame.height);
ctx.set_fill_color(theme.bg());
ctx.fill_rounded_rect(frame.x, frame.y, frame.width, frame.height, style.radius());
}
BackgroundFill::Texture { asset_id } => {
let _ = asset_id;
ctx.set_fill_color(theme.bg());
ctx.fill_rounded_rect(frame.x, frame.y, frame.width, frame.height, style.radius());
}
}
ctx.set_stroke_color(theme.border());
ctx.set_stroke_width(style.border_width());
ctx.set_line_dash(&[]);
ctx.stroke_rounded_rect(frame.x, frame.y, frame.width, frame.height, style.radius());
let has_header = layout.header.height > 0.0;
if has_header {
ctx.set_fill_color(theme.header_bg());
ctx.fill_rect(
layout.header.x,
layout.header.y,
layout.header.width,
layout.header.height,
);
let title = view.title.unwrap_or("");
let title_rect = Rect::new(
layout.header.x + style.padding(),
layout.header.y,
(layout.header.width - layout.close_btn.width - style.padding() * 2.0).max(0.0),
layout.header.height,
);
let title_color = theme.header_text();
let title_view = TextView {
text: title,
align: TextAlign::Left,
baseline: TextBaseline::Middle,
color: Some(title_color),
font: Some("14px sans-serif"),
overflow: TextOverflow::Ellipsis,
hovered: false,
};
draw_text(ctx, title_rect, &title_view, &TextSettings::default());
ctx.set_fill_color(theme.divider());
ctx.fill_rect(
layout.header.x,
layout.header.y + layout.header.height - 1.0,
layout.header.width,
1.0,
);
let dh_view = DragHandleView { rect: layout.drag_handle };
let dh_settings = DragHandleSettings::default();
draw_drag_handle(ctx, layout.drag_handle, &dh_view, &dh_settings, &DragHandleRenderKind::Invisible);
let close_view = CloseButtonView { hovered: state.hovered_close };
let close_settings = CloseButtonSettings {
theme: Box::new(DefaultCloseButtonTheme),
style: Box::new(DefaultCloseButtonStyle),
};
draw_close_button(
ctx,
layout.close_btn,
if state.hovered_close { WidgetState::Hovered } else { WidgetState::Normal },
&close_view,
&close_settings,
&CloseButtonRenderKind::Default,
);
}
if !view.tabs.is_empty() {
match kind {
ModalRenderKind::TopTabs => {
draw_top_tabs(ctx, &layout, view, state, settings);
}
ModalRenderKind::SideTabs => {
draw_side_tabs(ctx, &layout, view, state, settings);
}
_ => {}
}
}
if layout.footer.height > 0.0 {
ctx.set_fill_color(theme.footer_border());
ctx.fill_rect(layout.footer.x, layout.footer.y, layout.footer.width, 1.0);
ctx.set_fill_color(theme.footer_bg());
ctx.fill_rect(
layout.footer.x,
layout.footer.y + 1.0,
layout.footer.width,
layout.footer.height - 1.0,
);
}
if layout.footer.height > 0.0 && !view.footer_buttons.is_empty() {
draw_footer_buttons(ctx, &layout, view, state);
}
if matches!(kind, ModalRenderKind::Wizard) {
draw_wizard_nav(ctx, &layout, view, state, settings);
}
if matches!(view.overflow, crate::types::OverflowMode::Scrollbar) {
if let Some(track) = state.body_scroll_track {
use crate::ui::widgets::atomic::scrollbar::{
render::{draw_scrollbar, ScrollbarView, ScrollbarVisualState},
style::StandardScrollbarStyle,
theme::DefaultScrollbarTheme,
};
let style = StandardScrollbarStyle::default();
let theme = DefaultScrollbarTheme::default();
let visual_state = if state.scroll.is_dragging {
ScrollbarVisualState::Dragging
} else {
ScrollbarVisualState::Active
};
let sv = ScrollbarView {
content_height: state.body_content_h,
viewport_height: state.body_viewport_h,
scroll_offset: state.scroll.offset,
state: visual_state,
drag_pos_y: None,
style: &style,
theme: &theme,
};
let _ = draw_scrollbar(ctx, track, &sv);
}
}
}
pub fn draw_body_overflow_chevrons(
ctx: &mut dyn RenderContext,
rect: Rect,
state: &ModalState,
view: &ModalView<'_>,
settings: &ModalSettings,
kind: &ModalRenderKind,
) {
let frame = resolve_frame(rect, state, kind);
let body = body_rect(frame, view, settings, kind);
if body.width <= 0.0 || body.height <= 0.0 { return; }
let scroll = crate::ui::widgets::composite::overflow::BodyScrollState {
offset_x: state.body_scroll_x,
offset_y: state.scroll.offset,
content_w: state.body_content_w,
content_h: state.body_content_h,
};
let overflowing = scroll.overflows(body.width, body.height).any();
let want_chevrons = match view.overflow {
crate::types::OverflowMode::Chevrons => true,
crate::types::OverflowMode::Clip if overflowing => true,
_ => false,
};
if !want_chevrons { return; }
let theme = settings.theme.as_ref();
crate::ui::widgets::composite::overflow::draw_chevrons_helper(
ctx, body, &scroll, theme.bg(), theme.bg(),
);
}
fn resolve_frame(rect: Rect, state: &ModalState, kind: &ModalRenderKind) -> Rect {
if matches!(
kind,
ModalRenderKind::WithHeader
| ModalRenderKind::WithHeaderFooter
| ModalRenderKind::TopTabs
| ModalRenderKind::SideTabs
) && (state.position.0 != 0.0 || state.position.1 != 0.0)
{
Rect::new(state.position.0, state.position.1, rect.width, rect.height)
} else {
rect
}
}
fn effective_sidebar_width(
view: &ModalView<'_>,
style: &dyn super::style::ModalStyle,
kind: &ModalRenderKind,
) -> f64 {
if !matches!(kind, ModalRenderKind::SideTabs) {
return 0.0;
}
let base = style.sidebar_width();
let pad = 12.0_f64;
let max_label_len = view.tabs.iter()
.map(|t| t.len())
.max()
.unwrap_or(0) as f64;
let label_w = max_label_len * 7.0 + pad * 2.0;
base.max(label_w)
}
pub fn body_rect(
frame_rect: Rect,
view: &ModalView<'_>,
settings: &ModalSettings,
kind: &ModalRenderKind,
) -> Rect {
let style = settings.style.as_ref();
let has_header = matches!(
kind,
ModalRenderKind::WithHeader
| ModalRenderKind::WithHeaderFooter
| ModalRenderKind::TopTabs
| ModalRenderKind::SideTabs
);
let has_top_tabs = matches!(kind, ModalRenderKind::TopTabs);
let has_sidebar = matches!(kind, ModalRenderKind::SideTabs);
let has_footer = matches!(
kind,
ModalRenderKind::WithHeaderFooter | ModalRenderKind::SideTabs
) || (matches!(kind, ModalRenderKind::TopTabs) && !view.footer_buttons.is_empty());
let has_wizard = matches!(kind, ModalRenderKind::Wizard);
let header_h = if has_header { style.header_height() } else { 0.0 };
let tab_h = if has_top_tabs { style.tab_height() } else { 0.0 };
let footer_h = if has_footer { style.footer_height() } else { 0.0 };
let wizard_nav_h = if has_wizard { style.wizard_nav_height() } else { 0.0 };
let sidebar_w = if has_sidebar { effective_sidebar_width(view, style, kind) } else { 0.0 };
Rect::new(
frame_rect.x + sidebar_w,
frame_rect.y + header_h + tab_h,
(frame_rect.width - sidebar_w).max(0.0),
(frame_rect.height - header_h - tab_h - footer_h - wizard_nav_h).max(0.0),
)
}
pub fn measure_chrome(
view: &ModalView<'_>,
settings: &ModalSettings,
kind: &ModalRenderKind,
) -> (f64, f64) {
let style = settings.style.as_ref();
let has_header = matches!(
kind,
ModalRenderKind::WithHeader
| ModalRenderKind::WithHeaderFooter
| ModalRenderKind::TopTabs
| ModalRenderKind::SideTabs
);
let has_top_tabs = matches!(kind, ModalRenderKind::TopTabs);
let has_sidebar = matches!(kind, ModalRenderKind::SideTabs);
let has_footer = matches!(
kind,
ModalRenderKind::WithHeaderFooter | ModalRenderKind::SideTabs
) || (matches!(kind, ModalRenderKind::TopTabs) && !view.footer_buttons.is_empty());
let has_wizard = matches!(kind, ModalRenderKind::Wizard);
let header_h = if has_header { style.header_height() } else { 0.0 };
let tab_h = if has_top_tabs { style.tab_height() } else { 0.0 };
let footer_h = if has_footer { style.footer_height() } else { 0.0 };
let wizard_nav_h = if has_wizard { style.wizard_nav_height() } else { 0.0 };
let sidebar_w = if has_sidebar { effective_sidebar_width(view, style, kind) } else { 0.0 };
let extra_w = sidebar_w;
let extra_h = header_h + tab_h + footer_h + wizard_nav_h;
(extra_w, extra_h)
}
fn compute_layout(
frame: Rect,
_state: &ModalState,
view: &ModalView<'_>,
settings: &ModalSettings,
kind: &ModalRenderKind,
) -> ModalLayout {
let style = settings.style.as_ref();
let has_header = matches!(
kind,
ModalRenderKind::WithHeader
| ModalRenderKind::WithHeaderFooter
| ModalRenderKind::TopTabs
| ModalRenderKind::SideTabs
);
let has_close = has_header;
let has_top_tabs = matches!(kind, ModalRenderKind::TopTabs);
let has_sidebar = matches!(kind, ModalRenderKind::SideTabs);
let has_footer = matches!(
kind,
ModalRenderKind::WithHeaderFooter | ModalRenderKind::SideTabs
) || (matches!(kind, ModalRenderKind::TopTabs) && !view.footer_buttons.is_empty());
let has_wizard = matches!(kind, ModalRenderKind::Wizard);
let header_h = if has_header { style.header_height() } else { 0.0 };
let tab_h = if has_top_tabs { style.tab_height() } else { 0.0 };
let sidebar_w = if has_sidebar { effective_sidebar_width(view, style, kind) } else { 0.0 };
let footer_h = if has_footer { style.footer_height() } else { 0.0 };
let wizard_nav_h = if has_wizard { style.wizard_nav_height() } else { 0.0 };
let btn_size = style.close_btn_size();
let header = Rect::new(frame.x, frame.y, frame.width, header_h);
let close_btn = if has_close && header_h > 0.0 {
let padding = 10.0_f64;
Rect::new(
frame.x + frame.width - btn_size - padding,
frame.y + (header_h - btn_size) / 2.0,
btn_size,
btn_size,
)
} else {
Rect::default()
};
let drag_handle = if has_header && header_h > 0.0 {
Rect::new(
frame.x,
frame.y,
frame.width - close_btn.width,
header_h,
)
} else {
Rect::default()
};
let tab_strip = if has_top_tabs {
Rect::new(frame.x, frame.y + header_h, frame.width, tab_h)
} else {
Rect::default()
};
let sidebar = if has_sidebar {
Rect::new(
frame.x,
frame.y + header_h,
sidebar_w,
frame.height - header_h - footer_h,
)
} else {
Rect::default()
};
let footer = if has_footer || has_wizard {
Rect::new(
frame.x,
frame.y + frame.height - footer_h - wizard_nav_h,
frame.width,
footer_h,
)
} else {
Rect::default()
};
let wizard_nav = if has_wizard {
Rect::new(
frame.x,
frame.y + frame.height - wizard_nav_h,
frame.width,
wizard_nav_h,
)
} else {
Rect::default()
};
ModalLayout {
header,
close_btn,
drag_handle,
tab_strip,
sidebar,
footer,
wizard_nav,
}
}
fn draw_top_tabs(
ctx: &mut dyn RenderContext,
layout: &ModalLayout,
view: &ModalView<'_>,
state: &ModalState,
_settings: &ModalSettings,
) {
let tab_theme = DefaultTabTheme;
let tab_style = ModalHorizontalTabStyle::default();
let tab_count = view.tabs.len();
if tab_count == 0 {
return;
}
let tab_w = layout.tab_strip.width / tab_count as f64;
for (i, label) in view.tabs.iter().enumerate() {
let tab_rect = Rect::new(
layout.tab_strip.x + i as f64 * tab_w,
layout.tab_strip.y,
tab_w,
layout.tab_strip.height,
);
let is_active = i == state.active_tab;
let is_hovered = state.hovered_tab == Some(i);
let cfg = TabConfig {
id: format!("tab_{}", i),
label: label.to_string(),
active: is_active,
closable: false,
icon: None,
intrinsic_width: false,
};
let tab_view = TabView {
tab: &cfg,
hovered: is_hovered,
pressed: false,
close_btn_hovered: false,
};
draw_modal_horizontal_tab(ctx, tab_rect, &tab_view, &tab_style, &tab_theme);
}
}
fn draw_side_tabs(
ctx: &mut dyn RenderContext,
layout: &ModalLayout,
view: &ModalView<'_>,
state: &ModalState,
_settings: &ModalSettings,
) {
let tab_theme = DefaultTabTheme;
let tab_style = ModalSidebarTabStyle::default();
let tab_count = view.tabs.len();
if tab_count == 0 {
return;
}
let tab_h = layout.sidebar.height / tab_count as f64;
for (i, label) in view.tabs.iter().enumerate() {
let tab_rect = Rect::new(
layout.sidebar.x,
layout.sidebar.y + i as f64 * tab_h,
layout.sidebar.width,
tab_h,
);
let is_active = i == state.active_tab;
let is_hovered = state.hovered_tab == Some(i);
let cfg = TabConfig {
id: format!("tab_{}", i),
label: label.to_string(),
active: is_active,
closable: false,
icon: None,
intrinsic_width: false,
};
let tab_view = TabView {
tab: &cfg,
hovered: is_hovered,
pressed: false,
close_btn_hovered: false,
};
draw_modal_sidebar_tab(ctx, tab_rect, &tab_view, &tab_style, &tab_theme);
}
}
fn draw_footer_buttons(
ctx: &mut dyn RenderContext,
layout: &ModalLayout,
view: &ModalView<'_>,
state: &ModalState,
) {
let btn_theme = DefaultButtonTheme;
let btn_w = 80.0_f64;
let btn_h = 28.0_f64;
let btn_gap = 8.0_f64;
let padding_right = 16.0_f64;
let total_btns = view.footer_buttons.len();
let total_w = total_btns as f64 * btn_w
+ (total_btns.saturating_sub(1)) as f64 * btn_gap;
let start_x = layout.footer.x + layout.footer.width - total_w - padding_right;
let btn_y = layout.footer.y + (layout.footer.height - btn_h) / 2.0;
for (i, btn) in view.footer_buttons.iter().enumerate() {
let btn_x = start_x + i as f64 * (btn_w + btn_gap);
let btn_rect = Rect::new(btn_x, btn_y, btn_w, btn_h);
let hovered = state.footer_hovered == Some(i);
match btn.style {
FooterBtnStyle::Primary => {
let btn_view = PrimaryButtonView { text: btn.label, hovered };
draw_primary_button(ctx, btn_rect, &btn_view, 4.0, &btn_theme);
}
FooterBtnStyle::Ghost => {
let btn_view = GhostOutlineButtonView { text: btn.label, hovered };
draw_ghost_outline_button(ctx, btn_rect, &btn_view, 4.0, &btn_theme);
}
FooterBtnStyle::Danger => {
let btn_view = DangerButtonView {
text: btn.label,
hovered,
variant: DangerVariant::Delete,
icon: None,
};
draw_danger_button(
ctx,
btn_rect,
&btn_view,
4.0,
&btn_theme,
|_ctx, _icon, _rect, _color| {},
);
}
}
}
}
fn draw_wizard_nav(
ctx: &mut dyn RenderContext,
layout: &ModalLayout,
view: &ModalView<'_>,
state: &ModalState,
settings: &ModalSettings,
) {
let theme = settings.theme.as_ref();
let style = settings.style.as_ref();
let page_count = view.wizard_pages.len();
if page_count == 0 {
return;
}
let nav = layout.wizard_nav;
let dot_size = 8.0_f64;
let dot_gap = 6.0_f64;
let dots_total_w = page_count as f64 * dot_size
+ (page_count.saturating_sub(1)) as f64 * dot_gap;
let dots_x = nav.x + (nav.width - dots_total_w) / 2.0;
let dots_y = nav.y + nav.height / 2.0 - dot_size / 2.0;
for i in 0..page_count {
let cx = dots_x + i as f64 * (dot_size + dot_gap);
let color = if i == state.current_page {
theme.wizard_dot_active()
} else {
theme.wizard_dot_inactive()
};
ctx.set_fill_color(color);
ctx.fill_rounded_rect(cx, dots_y, dot_size, dot_size, dot_size / 2.0);
}
let btn_w = 72.0_f64;
let btn_h = 28.0_f64;
let btn_theme = DefaultButtonTheme;
let btn_y = nav.y + (nav.height - btn_h) / 2.0;
let padding_x = style.padding();
if state.current_page > 0 {
let back_rect = Rect::new(nav.x + padding_x, btn_y, btn_w, btn_h);
let hovered = state.footer_hovered == Some(0);
let back_view = GhostOutlineButtonView { text: "Back", hovered };
draw_ghost_outline_button(ctx, back_rect, &back_view, 4.0, &btn_theme);
}
let is_last = state.current_page + 1 >= page_count;
let next_label = if is_last { "Finish" } else { "Next" };
let next_rect = Rect::new(nav.x + nav.width - padding_x - btn_w, btn_y, btn_w, btn_h);
let hovered = state.footer_hovered == Some(1);
let next_view = PrimaryButtonView { text: next_label, hovered };
draw_primary_button(ctx, next_rect, &next_view, 4.0, &btn_theme);
}
const RESIZE_HANDLE_THICKNESS: f64 = 6.0;
pub fn register_body_overflow(
coord: &mut InputCoordinator,
modal_id: &CompositeId,
frame: Rect,
view: &ModalView<'_>,
settings: &ModalSettings,
kind: &ModalRenderKind,
state: &mut ModalState,
) {
let body = body_rect(frame, view, settings, kind);
if body.width <= 0.0 || body.height <= 0.0 {
return;
}
state.body_viewport_h = body.height;
state.body_viewport_w = body.width;
use crate::ui::widgets::composite::overflow::{
compute_compress_factor, register_chevrons_helper, register_scrollbar_helper,
BodyScrollState, CompressFactor, ScrollAxis,
};
let scroll = BodyScrollState {
offset_x: state.body_scroll_x,
offset_y: state.scroll.offset,
content_w: state.body_content_w,
content_h: state.body_content_h,
};
let layer = crate::input::core::coordinator::LayerId::main();
state.body_compress_factor = CompressFactor::one();
let overflowing = scroll.overflows(body.width, body.height).any();
let mut effective = match view.overflow {
crate::types::OverflowMode::Scrollbar => crate::types::OverflowMode::Scrollbar,
crate::types::OverflowMode::Chevrons => crate::types::OverflowMode::Chevrons,
crate::types::OverflowMode::Compress => {
let factor = compute_compress_factor(
state.body_content_w, state.body_content_h, body, 0.4,
);
state.body_compress_factor = factor;
crate::types::OverflowMode::Compress
}
_ if overflowing => crate::types::OverflowMode::Chevrons,
other => other,
};
if matches!(effective, crate::types::OverflowMode::Compress) {
effective = crate::types::OverflowMode::Clip;
}
match effective {
crate::types::OverflowMode::Scrollbar => {
let o = scroll.overflows(body.width, body.height);
if o.vertical {
if let Some(track) = register_scrollbar_helper(
coord, modal_id, body, &scroll, ScrollAxis::Vertical, &layer,
) {
state.body_scroll_track = Some(track);
}
}
if o.horizontal {
let _ = register_scrollbar_helper(
coord, modal_id, body, &scroll, ScrollAxis::Horizontal, &layer,
);
}
}
crate::types::OverflowMode::Chevrons => {
register_chevrons_helper(coord, modal_id, body, &scroll, &layer);
}
_ => {}
}
}
fn register_resize_handles(coord: &mut InputCoordinator, modal_id: &CompositeId, frame: Rect) {
let t = RESIZE_HANDLE_THICKNESS;
let edges = [
("resize_n", Rect::new(frame.x, frame.y, frame.width, t)),
("resize_s", Rect::new(frame.x, frame.y + frame.height - t, frame.width, t)),
("resize_w", Rect::new(frame.x, frame.y, t, frame.height)),
("resize_e", Rect::new(frame.x + frame.width - t, frame.y, t, frame.height)),
("resize_nw", Rect::new(frame.x, frame.y, t * 2.0, t * 2.0)),
("resize_ne", Rect::new(frame.x + frame.width - t * 2.0, frame.y, t * 2.0, t * 2.0)),
("resize_sw", Rect::new(frame.x, frame.y + frame.height - t * 2.0, t * 2.0, t * 2.0)),
("resize_se", Rect::new(frame.x + frame.width - t * 2.0, frame.y + frame.height - t * 2.0, t * 2.0, t * 2.0)),
];
for (suffix, rect) in edges {
coord.register_child(
modal_id,
format!("{}:{}", modal_id.0.0, suffix),
WidgetKind::DragHandle,
rect,
Sense::DRAG | Sense::HOVER,
);
}
}