#[cfg(feature = "accessibility")]
use crate::renderer::AccessibilityActionTarget;
use crate::renderer::{
HydroNativeView, HydroState, HydrolysisRenderer, RenderContext, RetainedSubview,
WidgetRenderContext, local_interaction_state, measure_label_intrinsic, measure_view_intrinsic,
popup_menu_nodes, resolved_color_to_peniko, transformed_rect,
};
#[cfg(feature = "accessibility")]
use accesskit::{
Action as AccessibilityAction, Node as AccessibilityNode, Role as AccessibilityNodeRole,
};
use nami::{Signal, SignalExt};
use std::cell::RefCell;
use std::rc::Rc;
use waterui::ViewExt as _;
use waterui::floating::FloatingScope;
use waterui::style::FloatingStyle;
use waterui_backend_core::widget::{ButtonMetrics, InteractionStyle};
use waterui_controls::button::{ButtonConfig, ButtonSize, ButtonStyle};
use waterui_controls::label::{Label, LabelDisplayMode};
use waterui_controls::menu::ResolvedMenu;
use waterui_core::layout::Point as LayoutPoint;
use waterui_core::layout::Size as LayoutSize;
use waterui_core::layout::{ProposalSize, ViewDimensions};
use waterui_core::{AnyView, Environment, Native};
use waterui_graphics::color::Color;
use waterui_text::styled::StyledStr;
use crate::widgets::util::{inset_rect, widget_disabled};
pub(crate) struct ButtonRenderState {
config: ButtonConfig,
label_view: Option<RetainedSubview>,
}
impl ButtonRenderState {
pub(crate) fn from_config(config: ButtonConfig) -> Self {
Self {
config,
label_view: None,
}
}
pub(crate) fn init_label(&mut self) {
if renders_as_plain_title(&self.config.label) {
return;
}
self.label_view = Some(RetainedSubview::new(AnyView::new(
self.config.label.clone(),
)));
}
pub(crate) fn ensure_label_built(
&mut self,
renderer: &mut HydrolysisRenderer,
env: &Environment,
) {
let Some(subview) = &mut self.label_view else {
return;
};
if subview.is_built() {
return;
}
let theme = renderer.theme();
let style = self.config.style;
let interaction_style = env.get::<InteractionStyle>();
let floating_style = env.get::<FloatingScope>().map(|scope| &scope.0);
let color = if env.get::<ListRowChrome>().is_some() {
Some(Color::new(waterui::theme::color::Foreground))
} else {
disabled_aware_label_color(
&theme,
style,
&widget_disabled(env),
interaction_style,
floating_style,
)
};
let styled = styled_button_label(&theme, style, self.config.label.clone());
subview.map_source(|_| button_label_view(color, AnyView::new(styled)));
subview.ensure_built(renderer, env);
}
}
impl HydroNativeView for Native<ButtonConfig> {
fn intrinsic(
state: &mut crate::renderer::HydroState,
view: &Self,
env: &Environment,
theme: &Rc<dyn crate::engine::WidgetTheme>,
) -> LayoutSize {
measure_button_intrinsic(view.as_inner(), state, env, theme)
}
}
pub(crate) fn button_accessibility(
renderer: &mut crate::renderer::SemanticCore,
ctx: Option<RenderContext>,
state: &Rc<RefCell<ButtonRenderState>>,
env: &Environment,
) {
#[cfg(feature = "accessibility")]
{
let button = &state.borrow().config;
let mut node = AccessibilityNode::new(renderer.resolve_accessibility_role(
env,
match button.style {
ButtonStyle::Link => AccessibilityNodeRole::Link,
_ => AccessibilityNodeRole::Button,
},
));
let default_label = renderer.accessibility_label_from_label(&button.label, env);
let label = renderer.resolve_accessibility_label(env, default_label);
if let Some(label) = label {
node.set_label(label);
}
node.add_action(AccessibilityAction::Focus);
let disabled = renderer.read_signal(&widget_disabled(env));
let action_target = if disabled {
node.set_disabled();
None
} else {
node.add_action(AccessibilityAction::Click);
Some(AccessibilityActionTarget::Activate {
action: button_activation(state, env),
})
};
let node_id = match ctx {
Some(ctx) => {
let bounds = transformed_rect(ctx.hit_transform, ctx.bounds);
renderer.register_accessibility_node(node, bounds, env, action_target)
}
None => renderer.register_accessibility_node_semantic(node, env, action_target),
};
if let Some(node_id) = node_id {
renderer.register_accessibility_focus_link(
&crate::renderer::InteractionKey::for_rc(state, 0),
node_id,
);
}
}
#[cfg(not(feature = "accessibility"))]
{
let _ = (renderer, ctx, state, env);
}
}
#[cfg(feature = "accessibility")]
pub(crate) fn button_activation(
state: &Rc<RefCell<ButtonRenderState>>,
env: &Environment,
) -> crate::renderer::AccessibilityActivation {
let state = Rc::clone(state);
let action_env = env.clone();
Rc::new(RefCell::new(
move |_renderer: &mut crate::renderer::SemanticCore, _env: &Environment| {
(state.borrow_mut().config.action)(&action_env);
true
},
))
}
fn button_chrome_size(
label_size: LayoutSize,
metrics: &ButtonMetrics,
proposal: ProposalSize,
) -> LayoutSize {
let content_width = f64::from(label_size.width) + metrics.padding_x * 2.0;
let content_height = f64::from(label_size.height) + metrics.padding_y * 2.0;
let min_width = proposal.width.map_or(metrics.min_width, |width| {
metrics.min_width.min(f64::from(width))
});
let min_height = proposal.height.map_or(metrics.min_height, |height| {
metrics.min_height.min(f64::from(height))
});
LayoutSize::new(
content_width.max(min_width) as f32,
content_height.max(min_height) as f32,
)
}
pub(crate) fn measure_button_node(
render_state: &ButtonRenderState,
proposal: ProposalSize,
state: &mut HydroState,
env: &Environment,
theme: &Rc<dyn crate::engine::WidgetTheme>,
) -> ViewDimensions {
let metrics = button_metrics(
theme,
render_state.config.style,
render_state.config.size,
env.get::<InteractionStyle>(),
env.get::<FloatingScope>().map(|scope| &scope.0),
);
let label_size = match &render_state.label_view {
Some(subview) => subview.measure_built(state, env, theme),
None => {
let styled = styled_button_title(
theme,
render_state.config.style,
&render_state.config.label,
env,
);
HydrolysisRenderer::measure_text_intrinsic_size(state, styled, env)
}
};
ViewDimensions::new(button_chrome_size(label_size, &metrics, proposal))
}
pub(crate) fn render_button_node(
ctx: &mut WidgetRenderContext<'_>,
state: &Rc<RefCell<ButtonRenderState>>,
env: &Environment,
) {
let hidden = env
.get::<waterui::accessibility::AccessibilityHidden>()
.is_some_and(waterui::accessibility::AccessibilityHidden::is_hidden);
if !hidden {
let render_ctx = ctx.render_context();
button_accessibility(ctx.renderer_mut(), Some(render_ctx), state, env);
}
render_button_parts(ctx, state, env);
}
const MENU_TRIGGER_STYLE: ButtonStyle = ButtonStyle::Automatic;
pub(crate) struct MenuRenderState {
label: MenuLabel,
items: nami::Computed<Vec<waterui_controls::menu::ResolvedMenuItem>>,
#[cfg(feature = "accessibility")]
accessibility_label: nami::Computed<StyledStr>,
}
enum MenuLabel {
Title(Label),
View(RetainedSubview),
}
impl MenuRenderState {
pub(crate) fn from_resolved(menu: ResolvedMenu) -> Self {
let ResolvedMenu {
label,
items,
#[cfg(feature = "accessibility")]
accessibility_label,
#[cfg(not(feature = "accessibility"))]
accessibility_label: _,
} = menu;
let label = match label.downcast::<Label>() {
Ok(label) if renders_as_plain_title(&label) => MenuLabel::Title(*label),
Ok(label) => MenuLabel::View(RetainedSubview::new(AnyView::new(*label))),
Err(view) => MenuLabel::View(RetainedSubview::new(view)),
};
Self {
label,
items,
#[cfg(feature = "accessibility")]
accessibility_label,
}
}
pub(crate) fn ensure_label_built(
&mut self,
renderer: &mut HydrolysisRenderer,
env: &Environment,
) {
let theme = renderer.theme();
if let MenuLabel::View(subview) = &mut self.label {
if subview.is_built() {
return;
}
let color = theme.button_label_color(MENU_TRIGGER_STYLE, false);
subview.map_source(|view| button_label_view(color, view));
subview.ensure_built(renderer, env);
}
}
}
impl HydroNativeView for Native<ResolvedMenu> {
fn intrinsic(
state: &mut crate::renderer::HydroState,
view: &Self,
env: &Environment,
theme: &Rc<dyn crate::engine::WidgetTheme>,
) -> LayoutSize {
measure_menu_intrinsic(view.as_inner(), state, env, theme)
}
}
pub(crate) fn menu_accessibility(
renderer: &mut crate::renderer::SemanticCore,
ctx: Option<RenderContext>,
theme: Option<&Rc<dyn crate::engine::WidgetTheme>>,
state: &Rc<RefCell<MenuRenderState>>,
env: &Environment,
) {
#[cfg(feature = "accessibility")]
{
let accessibility_label = state.borrow().accessibility_label.clone();
let mut node = AccessibilityNode::new(
renderer.resolve_accessibility_role(env, AccessibilityNodeRole::Button),
);
let default_label = Some(
renderer
.read_signal(&accessibility_label)
.to_plain()
.to_string(),
);
let label = renderer.resolve_accessibility_label(env, default_label);
if let Some(label) = label {
node.set_label(label);
}
node.add_action(AccessibilityAction::Focus);
node.add_action(AccessibilityAction::Click);
let request = ctx.as_ref().zip(theme).map(|(ctx, theme)| {
let bounds = transformed_rect(ctx.hit_transform, ctx.bounds);
(
LayoutPoint::new(bounds.x0 as f32, bounds.y1 as f32),
theme.text_context_menu_metrics(),
)
});
let items = state.borrow().items.clone();
let activation = AccessibilityActionTarget::Activate {
action: Rc::new(RefCell::new(
move |renderer: &mut crate::renderer::SemanticCore, env: &Environment| {
let nodes = popup_menu_nodes(&items.get());
match request {
Some((anchor, metrics)) => {
renderer.show_popup_menu_nodes(nodes, anchor, metrics, env);
}
None => {
renderer.activate_popup_menu_nodes(nodes, env);
}
}
true
},
)),
};
let node_id = match ctx {
Some(ctx) => {
let bounds = transformed_rect(ctx.hit_transform, ctx.bounds);
renderer.register_accessibility_node(node, bounds, env, Some(activation))
}
None => renderer.register_accessibility_node_semantic(node, env, Some(activation)),
};
if let Some(node_id) = node_id {
renderer.register_accessibility_focus_link(
&crate::renderer::InteractionKey::for_rc(state, 0),
node_id,
);
}
}
#[cfg(not(feature = "accessibility"))]
{
let _ = (renderer, ctx, theme, state, env);
}
}
pub(crate) fn measure_menu_node(
state: &MenuRenderState,
proposal: ProposalSize,
hydro: &mut HydroState,
env: &Environment,
theme: &Rc<dyn crate::engine::WidgetTheme>,
) -> ViewDimensions {
let metrics = theme.button_metrics(MENU_TRIGGER_STYLE, ButtonSize::default());
let label_size = match &state.label {
MenuLabel::Title(label) => {
let styled = styled_button_title(theme, MENU_TRIGGER_STYLE, label, env);
HydrolysisRenderer::measure_text_intrinsic_size(hydro, styled, env)
}
MenuLabel::View(subview) => subview.measure_built(hydro, env, theme),
};
ViewDimensions::new(button_chrome_size(label_size, &metrics, proposal))
}
pub(crate) fn render_menu_node(
ctx: &mut WidgetRenderContext<'_>,
state: &Rc<RefCell<MenuRenderState>>,
env: &Environment,
) {
let hidden = env
.get::<waterui::accessibility::AccessibilityHidden>()
.is_some_and(waterui::accessibility::AccessibilityHidden::is_hidden);
if !hidden {
let theme = ctx.theme();
let render_ctx = ctx.render_context();
menu_accessibility(
ctx.renderer_mut(),
Some(render_ctx),
Some(&theme),
state,
env,
);
}
render_menu_parts(ctx, state, env);
}
pub(crate) fn render_button_parts(
ctx: &mut WidgetRenderContext<'_>,
state: &Rc<RefCell<ButtonRenderState>>,
env: &Environment,
) {
let theme = ctx.theme();
let style = state.borrow().config.style;
let size = state.borrow().config.size;
let interaction_style = env.get::<InteractionStyle>().cloned();
let floating_style = env.get::<FloatingScope>().map(|scope| scope.0.clone());
{
let label = state.borrow().config.label.clone();
watch_button_title(ctx.renderer_mut(), &label, env);
}
let disabled = {
let signal = widget_disabled(env);
ctx.renderer_mut().read_signal(&signal)
};
let bounds = ctx.bounds;
let hit_bounds = transformed_rect(ctx.hit_transform, ctx.bounds);
let interaction_key = crate::renderer::InteractionKey::for_rc(state, 0);
let (interaction, press_slot, _) = ctx.renderer_mut().bind_control_interaction_target(
interaction_key,
hit_bounds,
env,
disabled,
);
if interaction_style.is_none() && floating_style.is_none() {
let mut draw = ctx.draw_context();
theme.draw_button_chrome(&mut draw, bounds, style, interaction);
}
let metrics = button_metrics(
&theme,
style,
size,
interaction_style.as_ref(),
floating_style.as_ref(),
);
let label_bounds = inset_rect(bounds, metrics.padding_x, metrics.padding_y);
let label_target = if label_bounds.width() > 0.0 && label_bounds.height() > 0.0 {
label_bounds
} else {
bounds
};
{
let mut state_mut = state.borrow_mut();
if let Some(subview) = &mut state_mut.label_view {
let render_ctx = ctx.render_context();
ctx.renderer_mut()
.with_suppressed_accessibility(|renderer| {
subview.flush_in_rect(
renderer,
render_ctx,
env,
ProposalSize::UNSPECIFIED,
label_target,
);
});
} else if label_target.width() > 0.0 && label_target.height() > 0.0 {
let mut styled = styled_button_title(&theme, style, &state_mut.config.label, env);
let title_color = if env.get::<ListRowChrome>().is_some() {
Some(Color::new(waterui::theme::color::Foreground))
} else {
button_label_color(
&theme,
style,
disabled,
interaction_style.as_ref(),
floating_style.as_ref(),
)
};
if let Some(color) = title_color {
styled = styled_with_default_foreground(styled, color);
}
ctx.render_styled_text_single_line_centered(styled, env, label_target);
}
}
{
let interaction = local_interaction_state(interaction, ctx.hit_transform);
if let Some(interaction_style) = interaction_style {
let color_signal = interaction_style.state_layer_color.resolve(env);
let color = resolved_color_to_peniko(ctx.renderer_mut().read_signal(&color_signal));
let mut draw = ctx.draw_context();
theme.draw_interaction_state_layer(
&mut draw,
interaction_style.state_layer_bounds(bounds),
interaction_style.state_layer_radii,
color,
interaction,
);
} else if let Some(floating_style) = floating_style {
let color_signal = floating_style.state_layer_color.resolve(env);
let color = resolved_color_to_peniko(ctx.renderer_mut().read_signal(&color_signal));
let corner_radius =
bounds.width().min(bounds.height()) * f64::from(floating_style.clip_radius);
let mut draw = ctx.draw_context();
theme.draw_interaction_state_layer(
&mut draw,
bounds,
corner_radius.into(),
color,
interaction,
);
} else {
let mut draw = ctx.draw_context();
theme.draw_button_state_layer(&mut draw, bounds, style, interaction);
}
}
if disabled {
return;
}
let state = Rc::clone(state);
let action_env = env.clone();
ctx.renderer_mut().register_interactive_pointer_target(
hit_bounds,
press_slot,
move |_renderer, _point, _env| {
(state.borrow_mut().config.action)(&action_env);
true
},
);
}
pub(crate) fn render_menu_parts(
ctx: &mut WidgetRenderContext<'_>,
state: &Rc<RefCell<MenuRenderState>>,
env: &Environment,
) {
let theme = ctx.theme();
let style = MENU_TRIGGER_STYLE;
let bounds = ctx.bounds;
let hit_bounds = transformed_rect(ctx.hit_transform, ctx.bounds);
let interaction_key = crate::renderer::InteractionKey::for_rc(state, 0);
let (interaction, press_slot, _) =
ctx.renderer_mut()
.bind_interaction_target(interaction_key, hit_bounds, env);
{
let mut draw = ctx.draw_context();
theme.draw_button_chrome(&mut draw, bounds, style, interaction);
}
let metrics = theme.button_metrics(style, ButtonSize::default());
let label_bounds = inset_rect(bounds, metrics.padding_x, metrics.padding_y);
{
let mut state = state.borrow_mut();
match &mut state.label {
MenuLabel::Title(label)
if label_bounds.width() > 0.0 && label_bounds.height() > 0.0 =>
{
let mut styled = styled_button_title(&theme, style, label, env);
if let Some(color) = theme.button_label_color(style, false) {
styled = styled_with_default_foreground(styled, color);
}
ctx.render_styled_text_single_line_centered(styled, env, label_bounds);
}
MenuLabel::Title(_) => {}
MenuLabel::View(subview) => {
let render_ctx = ctx.render_context();
ctx.renderer_mut()
.with_suppressed_accessibility(|renderer| {
subview.flush_in_rect(
renderer,
render_ctx,
env,
ProposalSize::UNSPECIFIED,
label_bounds,
);
});
}
}
}
{
let interaction = local_interaction_state(interaction, ctx.hit_transform);
let mut draw = ctx.draw_context();
theme.draw_button_state_layer(&mut draw, bounds, style, interaction);
}
let items = state.borrow().items.clone();
let _ = ctx.renderer_mut().read_signal(&items);
let anchor = LayoutPoint::new(hit_bounds.x0 as f32, hit_bounds.y1 as f32);
let menu_metrics = theme.text_context_menu_metrics();
ctx.renderer_mut().register_interactive_pointer_target(
hit_bounds,
press_slot,
move |renderer, _point, env| {
renderer.show_popup_menu_nodes(
popup_menu_nodes(&items.get()),
anchor,
menu_metrics,
env,
)
},
);
}
pub(crate) fn measure_button_intrinsic(
button: &ButtonConfig,
state: &mut HydroState,
env: &Environment,
theme: &Rc<dyn crate::engine::WidgetTheme>,
) -> LayoutSize {
let metrics = button_metrics(
theme,
button.style,
button.size,
env.get::<InteractionStyle>(),
env.get::<FloatingScope>().map(|scope| &scope.0),
);
let label_size = measure_button_label_intrinsic(theme, button.style, &button.label, state, env);
button_chrome_size(label_size, &metrics, ProposalSize::UNSPECIFIED)
}
pub(crate) fn measure_menu_intrinsic(
menu: &ResolvedMenu,
state: &mut HydroState,
env: &Environment,
theme: &Rc<dyn crate::engine::WidgetTheme>,
) -> LayoutSize {
let metrics = theme.button_metrics(MENU_TRIGGER_STYLE, ButtonSize::default());
let label_size = if let Some(label) = menu.label.downcast_ref::<Label>()
&& renders_as_plain_title(label)
{
let styled = styled_button_title(theme, MENU_TRIGGER_STYLE, label, env);
HydrolysisRenderer::measure_text_intrinsic_size(state, styled, env)
} else {
measure_view_intrinsic(&menu.label, state, env, theme)
};
button_chrome_size(label_size, &metrics, ProposalSize::UNSPECIFIED)
}
fn button_label_view(color: Option<Color>, label: AnyView) -> AnyView {
match color {
Some(color) => AnyView::new(label.foreground(color)),
None => label,
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct ListRowChrome;
fn disabled_aware_label_color(
theme: &Rc<dyn crate::engine::WidgetTheme>,
style: ButtonStyle,
disabled: &nami::Computed<bool>,
interaction_style: Option<&InteractionStyle>,
floating_style: Option<&FloatingStyle>,
) -> Option<Color> {
let enabled_color = button_label_color(theme, style, false, interaction_style, floating_style);
let disabled_color = button_label_color(theme, style, true, interaction_style, floating_style);
match (enabled_color, disabled_color) {
(None, None) => None,
(Some(when_false), Some(when_true)) => Some(Color::new(SelectResolvedColor {
condition: disabled.clone(),
when_true,
when_false,
})),
(enabled_color, disabled_color) => panic!(
"theme must override the button label color for both the enabled and \
disabled states, or neither (enabled: {enabled_color:?}, disabled: {disabled_color:?})"
),
}
}
fn button_metrics(
theme: &Rc<dyn crate::engine::WidgetTheme>,
style: ButtonStyle,
size: ButtonSize,
interaction_style: Option<&InteractionStyle>,
floating_style: Option<&FloatingStyle>,
) -> ButtonMetrics {
interaction_style.map_or_else(
|| {
floating_style.map_or_else(
|| theme.button_metrics(style, size),
|style| {
ButtonMetrics::new(
style.content_inset_x,
style.content_inset_y,
style.minimum_width,
style.minimum_height,
)
},
)
},
|style| style.metrics,
)
}
fn button_label_color(
theme: &Rc<dyn crate::engine::WidgetTheme>,
style: ButtonStyle,
disabled: bool,
interaction_style: Option<&InteractionStyle>,
floating_style: Option<&FloatingStyle>,
) -> Option<Color> {
interaction_style.map_or_else(
|| {
floating_style.map_or_else(
|| theme.button_label_color(style, disabled),
|style| {
Some(if disabled {
style
.content_color
.clone()
.with_opacity(style.disabled_content_opacity)
} else {
style.content_color.clone()
})
},
)
},
|style| style.resolved_label_color(disabled),
)
}
#[derive(Debug, Clone)]
struct SelectResolvedColor {
condition: nami::Computed<bool>,
when_true: Color,
when_false: Color,
}
impl waterui_core::resolve::Resolvable for SelectResolvedColor {
type Resolved = waterui_graphics::color::ResolvedColor;
fn resolve(&self, env: &Environment) -> impl Signal<Output = Self::Resolved> {
let when_true = self.when_true.resolve(env);
let when_false = self.when_false.resolve(env);
nami::zip::zip(
nami::zip::zip(self.condition.clone(), when_true),
when_false,
)
.map(
|((condition, when_true), when_false)| {
if condition { when_true } else { when_false }
},
)
}
}
fn measure_button_label_intrinsic(
theme: &Rc<dyn crate::engine::WidgetTheme>,
style: ButtonStyle,
label: &Label,
state: &mut HydroState,
env: &Environment,
) -> LayoutSize {
if renders_as_plain_title(label) {
let styled = styled_button_title(theme, style, label, env);
HydrolysisRenderer::measure_text_intrinsic_size(state, styled, env)
} else {
let label = styled_button_label(theme, style, label.clone());
measure_label_intrinsic(&label, state, env, theme)
}
}
fn renders_as_plain_title(label: &Label) -> bool {
matches!(label.display_mode_preference(), LabelDisplayMode::TitleOnly)
&& !label.has_custom_content()
}
fn styled_button_title(
theme: &Rc<dyn crate::engine::WidgetTheme>,
style: ButtonStyle,
label: &Label,
env: &Environment,
) -> StyledStr {
let title = label.semantic_text().clone();
let title = if let Some(font) = theme.button_label_font(style) {
title.font(font)
} else {
title
};
title.resolve(env).content.get()
}
fn watch_button_title(renderer: &mut HydrolysisRenderer, label: &Label, env: &Environment) {
let _ = renderer.read_signal(&label.semantic_text().resolve(env).content);
}
fn styled_with_default_foreground(styled: StyledStr, color: Color) -> StyledStr {
let mut out = StyledStr::empty();
for (chunk, style) in styled.chunks() {
let mut style = style.clone();
if style.foreground.is_none() {
style.foreground = Some(color.clone());
}
out.push(chunk.clone(), style);
}
out
}
fn styled_button_label(
theme: &Rc<dyn crate::engine::WidgetTheme>,
style: ButtonStyle,
label: Label,
) -> Label {
if label.has_custom_content() {
return label;
}
if let Some(font) = theme.button_label_font(style) {
label.font(font)
} else {
label
}
}
#[cfg(feature = "accessibility")]
pub(crate) fn emit_button_accessibility(
renderer: &mut crate::renderer::SemanticCore,
state: &Rc<RefCell<ButtonRenderState>>,
env: &Environment,
) {
button_accessibility(renderer, None, state, env);
}
#[cfg(feature = "accessibility")]
pub(crate) fn emit_menu_accessibility(
renderer: &mut crate::renderer::SemanticCore,
state: &Rc<RefCell<MenuRenderState>>,
env: &Environment,
) {
menu_accessibility(renderer, None, None, state, env);
}
#[cfg(test)]
mod tests {
use super::{MENU_TRIGGER_STYLE, button_chrome_size};
use waterui_backend_core::widget::ButtonMetrics;
use waterui_controls::button::ButtonStyle;
use waterui_core::layout::{ProposalSize, Size};
#[test]
fn menu_trigger_uses_material_default_button_style() {
assert_eq!(MENU_TRIGGER_STYLE, ButtonStyle::Automatic);
}
#[test]
fn button_chrome_measurement_uses_metrics_and_proposal() {
let label = Size::new(11.0, 13.0);
let metrics = ButtonMetrics::new(3.0, 5.0, 37.0, 41.0);
assert_eq!(
button_chrome_size(label, &metrics, ProposalSize::UNSPECIFIED),
Size::new(37.0, 41.0)
);
assert_eq!(
button_chrome_size(label, &metrics, ProposalSize::new(Some(31.0), Some(29.0))),
Size::new(31.0, 29.0)
);
}
}