2026-06-16 18:59:13 +03:00
|
|
|
|
//! Shared in-canvas modal overlay.
|
|
|
|
|
|
//!
|
|
|
|
|
|
//! Former pop-up *windows* (layer manager, style editors, About, …) render as
|
|
|
|
|
|
//! centered overlays on top of the main view instead of separate OS windows.
|
|
|
|
|
|
//! The native build has one main window and the web build has only the canvas,
|
|
|
|
|
|
//! so both stack dialogs here — one code path for every platform.
|
|
|
|
|
|
|
|
|
|
|
|
use crate::app::Message;
|
2026-07-30 14:47:28 +03:00
|
|
|
|
use iced::advanced::layout::{self, Layout};
|
|
|
|
|
|
use iced::advanced::widget::{self, Widget};
|
|
|
|
|
|
use iced::advanced::{mouse, overlay, renderer, Shell};
|
2026-08-01 18:24:05 +03:00
|
|
|
|
use iced::widget::{button, column, container, mouse_area, opaque, row, sensor, stack, Space};
|
2026-07-30 14:47:28 +03:00
|
|
|
|
use iced::{
|
|
|
|
|
|
Background, Border, Element, Event, Length, Padding, Rectangle, Renderer, Size, Theme, Vector,
|
|
|
|
|
|
};
|
2026-06-16 18:59:13 +03:00
|
|
|
|
|
2026-07-30 14:47:28 +03:00
|
|
|
|
#[derive(Debug, Clone, Copy)]
|
|
|
|
|
|
pub struct ModalOptions {
|
|
|
|
|
|
movable: bool,
|
|
|
|
|
|
resizable: bool,
|
|
|
|
|
|
neutral_close: bool,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Sizing used inside an intrinsically measured modal.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// [`INTRINSIC`](Self::INTRINSIC) lets each widget report the most space it can
|
|
|
|
|
|
/// use, while [`FILL`](Self::FILL) distributes the resulting shared frame to
|
|
|
|
|
|
/// sibling toolbars, panes and scroll areas.
|
|
|
|
|
|
#[derive(Debug, Clone, Copy)]
|
|
|
|
|
|
pub struct ModalSizing {
|
|
|
|
|
|
pub width: Length,
|
|
|
|
|
|
pub height: Length,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl ModalSizing {
|
|
|
|
|
|
pub const INTRINSIC: Self = Self {
|
|
|
|
|
|
width: Length::Fluid(iced_core::length::Constraint::Max),
|
|
|
|
|
|
height: Length::Fluid(iced_core::length::Constraint::Max),
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
pub const FILL: Self = Self {
|
|
|
|
|
|
width: Length::Fill,
|
|
|
|
|
|
height: Length::Fill,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Measure an intrinsic copy of `content`, then lay the fill copy out in the
|
|
|
|
|
|
/// resulting shared frame. `extra` grows that frame when the resize handle is
|
|
|
|
|
|
/// dragged; `max` only caps the initial intrinsic pass.
|
|
|
|
|
|
pub fn intrinsic<'a>(
|
|
|
|
|
|
measurement: impl Into<Element<'a, Message>>,
|
|
|
|
|
|
content: impl Into<Element<'a, Message>>,
|
|
|
|
|
|
max: Size,
|
|
|
|
|
|
extra: Vector,
|
|
|
|
|
|
) -> Element<'a, Message> {
|
|
|
|
|
|
Element::new(Intrinsic {
|
|
|
|
|
|
children: [measurement.into(), content.into()],
|
|
|
|
|
|
max,
|
|
|
|
|
|
extra,
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
struct Intrinsic<'a> {
|
|
|
|
|
|
children: [Element<'a, Message>; 2],
|
|
|
|
|
|
max: Size,
|
|
|
|
|
|
extra: Vector,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl Widget<Message, Theme, Renderer> for Intrinsic<'_> {
|
|
|
|
|
|
fn diff(&mut self, tree: &mut widget::Tree) {
|
|
|
|
|
|
tree.diff_children(&mut self.children);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn size(&self) -> Size<Length> {
|
|
|
|
|
|
Size::new(
|
|
|
|
|
|
Length::Fit.max(self.max.width + self.extra.x),
|
|
|
|
|
|
Length::Fit.max(self.max.height + self.extra.y),
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn layout(
|
|
|
|
|
|
&mut self,
|
|
|
|
|
|
tree: &mut widget::Tree,
|
|
|
|
|
|
renderer: &Renderer,
|
|
|
|
|
|
limits: &layout::Limits,
|
|
|
|
|
|
) -> layout::Node {
|
|
|
|
|
|
let measure_max = Size::new(
|
|
|
|
|
|
self.max.width.min(limits.max().width),
|
|
|
|
|
|
self.max.height.min(limits.max().height),
|
|
|
|
|
|
);
|
|
|
|
|
|
let measure_limits = layout::Limits::new(Size::ZERO, measure_max);
|
|
|
|
|
|
let measured = self.children[0]
|
|
|
|
|
|
.as_widget_mut()
|
|
|
|
|
|
.layout(&mut tree.children[0], renderer, &measure_limits);
|
|
|
|
|
|
let size = Size::new(
|
|
|
|
|
|
(measured.size().width + self.extra.x).min(limits.max().width),
|
|
|
|
|
|
(measured.size().height + self.extra.y).min(limits.max().height),
|
|
|
|
|
|
);
|
|
|
|
|
|
let final_limits = layout::Limits::new(size, size);
|
|
|
|
|
|
let content = self.children[1]
|
|
|
|
|
|
.as_widget_mut()
|
|
|
|
|
|
.layout(&mut tree.children[1], renderer, &final_limits);
|
|
|
|
|
|
|
|
|
|
|
|
layout::Node::with_children(size, vec![measured, content])
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn operate(
|
|
|
|
|
|
&mut self,
|
|
|
|
|
|
tree: &mut widget::Tree,
|
|
|
|
|
|
layout: Layout<'_>,
|
|
|
|
|
|
renderer: &Renderer,
|
|
|
|
|
|
operation: &mut dyn widget::Operation,
|
|
|
|
|
|
) {
|
|
|
|
|
|
self.children[1]
|
|
|
|
|
|
.as_widget_mut()
|
|
|
|
|
|
.operate(&mut tree.children[1], layout.child(1), renderer, operation);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn update(
|
|
|
|
|
|
&mut self,
|
|
|
|
|
|
tree: &mut widget::Tree,
|
|
|
|
|
|
event: &Event,
|
|
|
|
|
|
layout: Layout<'_>,
|
|
|
|
|
|
cursor: mouse::Cursor,
|
|
|
|
|
|
renderer: &Renderer,
|
|
|
|
|
|
shell: &mut Shell<'_, Message>,
|
|
|
|
|
|
viewport: &Rectangle,
|
|
|
|
|
|
) {
|
|
|
|
|
|
self.children[1].as_widget_mut().update(
|
|
|
|
|
|
&mut tree.children[1],
|
|
|
|
|
|
event,
|
|
|
|
|
|
layout.child(1),
|
|
|
|
|
|
cursor,
|
|
|
|
|
|
renderer,
|
|
|
|
|
|
shell,
|
|
|
|
|
|
viewport,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn mouse_interaction(
|
|
|
|
|
|
&self,
|
|
|
|
|
|
tree: &widget::Tree,
|
|
|
|
|
|
layout: Layout<'_>,
|
|
|
|
|
|
cursor: mouse::Cursor,
|
|
|
|
|
|
viewport: &Rectangle,
|
|
|
|
|
|
renderer: &Renderer,
|
|
|
|
|
|
) -> mouse::Interaction {
|
|
|
|
|
|
self.children[1]
|
|
|
|
|
|
.as_widget()
|
|
|
|
|
|
.mouse_interaction(
|
|
|
|
|
|
&tree.children[1],
|
|
|
|
|
|
layout.child(1),
|
|
|
|
|
|
cursor,
|
|
|
|
|
|
viewport,
|
|
|
|
|
|
renderer,
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn draw(
|
|
|
|
|
|
&self,
|
|
|
|
|
|
tree: &widget::Tree,
|
|
|
|
|
|
renderer: &mut Renderer,
|
|
|
|
|
|
theme: &Theme,
|
|
|
|
|
|
style: &renderer::Style,
|
|
|
|
|
|
layout: Layout<'_>,
|
|
|
|
|
|
cursor: mouse::Cursor,
|
|
|
|
|
|
viewport: &Rectangle,
|
|
|
|
|
|
) {
|
|
|
|
|
|
self.children[1]
|
|
|
|
|
|
.as_widget()
|
|
|
|
|
|
.draw(
|
|
|
|
|
|
&tree.children[1],
|
|
|
|
|
|
renderer,
|
|
|
|
|
|
theme,
|
|
|
|
|
|
style,
|
|
|
|
|
|
layout.child(1),
|
|
|
|
|
|
cursor,
|
|
|
|
|
|
viewport,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn overlay<'b>(
|
|
|
|
|
|
&'b mut self,
|
|
|
|
|
|
tree: &'b mut widget::Tree,
|
|
|
|
|
|
layout: Layout<'b>,
|
|
|
|
|
|
renderer: &Renderer,
|
|
|
|
|
|
viewport: &Rectangle,
|
|
|
|
|
|
translation: Vector,
|
|
|
|
|
|
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
|
|
|
|
|
|
self.children[1]
|
|
|
|
|
|
.as_widget_mut()
|
|
|
|
|
|
.overlay(
|
|
|
|
|
|
&mut tree.children[1],
|
|
|
|
|
|
layout.child(1),
|
|
|
|
|
|
renderer,
|
|
|
|
|
|
viewport,
|
|
|
|
|
|
translation,
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl ModalOptions {
|
|
|
|
|
|
pub const STANDARD: Self = Self {
|
|
|
|
|
|
movable: true,
|
|
|
|
|
|
resizable: true,
|
|
|
|
|
|
neutral_close: false,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
pub const NOTICE: Self = Self {
|
|
|
|
|
|
movable: true,
|
|
|
|
|
|
resizable: false,
|
|
|
|
|
|
neutral_close: true,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-30 15:19:58 +03:00
|
|
|
|
/// Dim and block the application beneath an overlay-owned dialog.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// Some third-party overlays ignore pointer movement outside their interactive
|
|
|
|
|
|
/// controls. The shield owns the cursor and pointer presses across the window,
|
|
|
|
|
|
/// making such an overlay behave modally.
|
|
|
|
|
|
pub fn backdrop<'a>(
|
|
|
|
|
|
base: impl Into<Element<'a, Message>>,
|
|
|
|
|
|
on_close: Message,
|
|
|
|
|
|
) -> Element<'a, Message> {
|
|
|
|
|
|
let shield = mouse_area(
|
|
|
|
|
|
container(Space::new())
|
|
|
|
|
|
.width(Length::Fill)
|
|
|
|
|
|
.height(Length::Fill)
|
|
|
|
|
|
.style(|theme: &Theme| container::Style {
|
|
|
|
|
|
background: Some(Background::Color(
|
|
|
|
|
|
theme
|
|
|
|
|
|
.palette()
|
|
|
|
|
|
.background
|
|
|
|
|
|
.strongest
|
|
|
|
|
|
.color
|
|
|
|
|
|
.scale_alpha(0.55),
|
|
|
|
|
|
)),
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
}),
|
|
|
|
|
|
)
|
|
|
|
|
|
.on_press(on_close)
|
|
|
|
|
|
.interaction(iced::mouse::Interaction::Idle);
|
|
|
|
|
|
|
|
|
|
|
|
stack![base.into(), opaque(shield)].into()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-30 14:47:28 +03:00
|
|
|
|
/// Stack `content` over `base` behind a dimmed backdrop, framed with a title bar
|
|
|
|
|
|
/// (the ✕ close button at its right end). The backdrop only dims and blocks
|
|
|
|
|
|
/// clicks from reaching the view beneath — it does **not** dismiss the dialog;
|
|
|
|
|
|
/// closing is the ✕ button alone (`on_close`).
|
2026-06-17 09:59:17 +03:00
|
|
|
|
///
|
|
|
|
|
|
/// `offset` shifts the dialog from screen-centre so it can be dragged by its
|
|
|
|
|
|
/// title bar; pass `Vector::ZERO` to keep it centred.
|
2026-06-16 18:59:13 +03:00
|
|
|
|
pub fn modal<'a>(
|
|
|
|
|
|
base: impl Into<Element<'a, Message>>,
|
2026-07-21 23:00:36 +03:00
|
|
|
|
title: &'a str,
|
2026-06-16 18:59:13 +03:00
|
|
|
|
content: impl Into<Element<'a, Message>>,
|
|
|
|
|
|
on_close: Message,
|
2026-06-17 09:59:17 +03:00
|
|
|
|
offset: Vector,
|
2026-07-30 14:47:28 +03:00
|
|
|
|
options: ModalOptions,
|
2026-06-16 18:59:13 +03:00
|
|
|
|
) -> Element<'a, Message> {
|
2026-07-30 14:47:28 +03:00
|
|
|
|
let close = if options.neutral_close {
|
|
|
|
|
|
button(crate::ui::icons::themed_secondary(
|
|
|
|
|
|
crate::ui::icons::CLOSE,
|
|
|
|
|
|
13.0,
|
|
|
|
|
|
))
|
|
|
|
|
|
.on_press(on_close)
|
|
|
|
|
|
.padding([1, 7])
|
|
|
|
|
|
.style(neutral_close_style)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
button(crate::ui::icons::themed_danger(
|
|
|
|
|
|
crate::ui::icons::CLOSE,
|
|
|
|
|
|
13.0,
|
|
|
|
|
|
))
|
|
|
|
|
|
.on_press(on_close)
|
|
|
|
|
|
.padding([1, 7])
|
|
|
|
|
|
.style(close_style)
|
|
|
|
|
|
};
|
2026-06-17 09:59:17 +03:00
|
|
|
|
|
2026-07-30 14:47:28 +03:00
|
|
|
|
// The dialog name is centred across the content width with the ✕ overlaid
|
|
|
|
|
|
// at the right edge. The title bar is itself an overlay: the content layer
|
|
|
|
|
|
// below determines the intrinsic modal width before `Fill` is resolved.
|
2026-07-28 09:33:12 +03:00
|
|
|
|
let title_text = iced::widget::text(title).size(15);
|
2026-07-30 14:47:28 +03:00
|
|
|
|
let title_surface: Element<'a, Message> = container(title_text)
|
|
|
|
|
|
.width(Length::Fill)
|
|
|
|
|
|
.height(Length::Fixed(24.0))
|
|
|
|
|
|
.align_x(iced::alignment::Horizontal::Center)
|
|
|
|
|
|
.align_y(iced::alignment::Vertical::Center)
|
|
|
|
|
|
.into();
|
|
|
|
|
|
let title_surface: Element<'a, Message> = if options.movable {
|
|
|
|
|
|
mouse_area(title_surface)
|
|
|
|
|
|
.on_press(Message::ModalGrab)
|
|
|
|
|
|
.interaction(iced::mouse::Interaction::Grab)
|
|
|
|
|
|
.into()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
title_surface
|
|
|
|
|
|
};
|
|
|
|
|
|
let controls: Element<'a, Message> = row![close].align_y(iced::Center).into();
|
2026-07-21 23:00:36 +03:00
|
|
|
|
let title_bar = stack![
|
2026-07-30 14:47:28 +03:00
|
|
|
|
title_surface,
|
|
|
|
|
|
container(controls)
|
|
|
|
|
|
.width(Length::Fill)
|
2026-07-21 23:00:36 +03:00
|
|
|
|
.height(Length::Fixed(24.0))
|
|
|
|
|
|
.align_x(iced::alignment::Horizontal::Right)
|
|
|
|
|
|
.align_y(iced::alignment::Vertical::Center),
|
2026-07-30 14:47:28 +03:00
|
|
|
|
]
|
|
|
|
|
|
.width(Length::Fill)
|
|
|
|
|
|
.height(Length::Fixed(24.0));
|
2026-06-17 09:59:17 +03:00
|
|
|
|
|
2026-07-28 09:33:12 +03:00
|
|
|
|
let panel_style = |theme: &Theme| container::Style {
|
|
|
|
|
|
background: Some(Background::Color(
|
2026-07-30 14:47:28 +03:00
|
|
|
|
theme.palette().background.base.color,
|
2026-07-28 09:33:12 +03:00
|
|
|
|
)),
|
2026-07-19 15:47:09 +03:00
|
|
|
|
border: Border {
|
2026-07-30 14:47:28 +03:00
|
|
|
|
color: theme.palette().background.neutral.color,
|
2026-07-19 15:47:09 +03:00
|
|
|
|
width: 1.0,
|
|
|
|
|
|
radius: 6.0.into(),
|
|
|
|
|
|
},
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-07-30 14:47:28 +03:00
|
|
|
|
// The first stack layer dictates its intrinsic size. Its top spacer reserves
|
|
|
|
|
|
// room for the title, while the actual title bar and resize grip overlay it
|
|
|
|
|
|
// without influencing the modal dimensions.
|
2026-08-01 18:24:05 +03:00
|
|
|
|
let measured_content = sensor(content).on_resize(Message::ModalContentResized);
|
2026-07-30 14:47:28 +03:00
|
|
|
|
let body_base = column![
|
|
|
|
|
|
Space::new().height(Length::Fixed(24.0)),
|
2026-08-01 18:24:05 +03:00
|
|
|
|
measured_content,
|
2026-07-30 14:47:28 +03:00
|
|
|
|
]
|
|
|
|
|
|
.spacing(6);
|
|
|
|
|
|
let mut body = stack![body_base, title_bar];
|
|
|
|
|
|
if options.resizable {
|
2026-07-19 15:47:09 +03:00
|
|
|
|
let resize = mouse_area(
|
2026-07-28 09:33:12 +03:00
|
|
|
|
container(crate::ui::icons::themed_primary(crate::ui::icons::RESIZE, 15.0))
|
2026-07-19 15:47:09 +03:00
|
|
|
|
.padding([0, 2]),
|
|
|
|
|
|
)
|
|
|
|
|
|
.on_press(Message::ModalResizeGrab)
|
|
|
|
|
|
.interaction(iced::mouse::Interaction::Grab);
|
2026-07-30 14:47:28 +03:00
|
|
|
|
body = body.push(
|
|
|
|
|
|
container(resize)
|
|
|
|
|
|
.width(Length::Fill)
|
|
|
|
|
|
.height(Length::Fill)
|
|
|
|
|
|
.align_x(iced::alignment::Horizontal::Right)
|
|
|
|
|
|
.align_y(iced::alignment::Vertical::Bottom),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
let framed: Element<'a, Message> =
|
|
|
|
|
|
container(body).padding(10).style(panel_style).into();
|
2026-06-17 09:59:17 +03:00
|
|
|
|
|
|
|
|
|
|
// Position via asymmetric padding (padding is non-negative): shifting a
|
|
|
|
|
|
// centred box by `d` on an axis needs (near − far) padding = 2·d there.
|
|
|
|
|
|
let pad = Padding {
|
|
|
|
|
|
top: offset.y.max(0.0) * 2.0,
|
|
|
|
|
|
right: (-offset.x).max(0.0) * 2.0,
|
|
|
|
|
|
bottom: (-offset.y).max(0.0) * 2.0,
|
|
|
|
|
|
left: offset.x.max(0.0) * 2.0,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// The backdrop fills the screen so dragging keeps tracking the cursor even
|
|
|
|
|
|
// when it leaves the title bar; release anywhere ends the drag.
|
|
|
|
|
|
let backdrop = mouse_area(
|
|
|
|
|
|
container(framed)
|
|
|
|
|
|
.center_x(Length::Fill)
|
|
|
|
|
|
.center_y(Length::Fill)
|
|
|
|
|
|
.padding(pad)
|
2026-07-28 09:33:12 +03:00
|
|
|
|
.style(|theme: &Theme| container::Style {
|
|
|
|
|
|
background: Some(Background::Color(
|
|
|
|
|
|
theme
|
2026-07-30 14:47:28 +03:00
|
|
|
|
.palette()
|
2026-07-28 09:33:12 +03:00
|
|
|
|
.background
|
|
|
|
|
|
.strongest
|
|
|
|
|
|
.color
|
|
|
|
|
|
.scale_alpha(0.55),
|
|
|
|
|
|
)),
|
2026-06-17 09:59:17 +03:00
|
|
|
|
..Default::default()
|
|
|
|
|
|
}),
|
|
|
|
|
|
)
|
|
|
|
|
|
.on_move(Message::ModalDragMove)
|
|
|
|
|
|
.on_release(Message::ModalDragRelease);
|
2026-06-16 18:59:13 +03:00
|
|
|
|
|
|
|
|
|
|
stack![
|
|
|
|
|
|
base.into(),
|
|
|
|
|
|
// `opaque` blocks pointer events from passing through, so the dimmed
|
|
|
|
|
|
// backdrop swallows clicks instead of closing or hitting the view.
|
2026-06-17 09:59:17 +03:00
|
|
|
|
opaque(backdrop),
|
2026-06-16 18:59:13 +03:00
|
|
|
|
]
|
|
|
|
|
|
.into()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-28 09:33:12 +03:00
|
|
|
|
fn close_style(theme: &Theme, status: button::Status) -> button::Style {
|
2026-07-30 14:47:28 +03:00
|
|
|
|
let palette = theme.palette();
|
2026-07-28 09:33:12 +03:00
|
|
|
|
let pair = match status {
|
|
|
|
|
|
button::Status::Hovered | button::Status::Pressed => palette.danger.strong,
|
|
|
|
|
|
_ => palette.background.weakest,
|
2026-06-16 18:59:13 +03:00
|
|
|
|
};
|
|
|
|
|
|
button::Style {
|
2026-07-28 09:33:12 +03:00
|
|
|
|
background: Some(Background::Color(pair.color)),
|
|
|
|
|
|
text_color: pair.text,
|
2026-06-16 18:59:13 +03:00
|
|
|
|
border: Border {
|
|
|
|
|
|
radius: 4.0.into(),
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
},
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-07-30 14:47:28 +03:00
|
|
|
|
|
|
|
|
|
|
fn neutral_close_style(theme: &Theme, status: button::Status) -> button::Style {
|
|
|
|
|
|
let palette = theme.palette();
|
|
|
|
|
|
let pair = match status {
|
|
|
|
|
|
button::Status::Hovered | button::Status::Pressed => palette.background.weak,
|
|
|
|
|
|
_ => palette.background.weakest,
|
|
|
|
|
|
};
|
|
|
|
|
|
button::Style {
|
|
|
|
|
|
background: Some(Background::Color(pair.color)),
|
|
|
|
|
|
text_color: pair.text,
|
|
|
|
|
|
border: Border {
|
|
|
|
|
|
radius: 4.0.into(),
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
},
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|