use std::cell::Cell;
use std::rc::Rc;
use teksilo_i18n::lit;
use teksilo_canvas::{Rect, Size, SizeProposal};
use teksilo_core::PlatformTitleBarHost;
use teksilo_core::accessibility::AccessNodeBuilder;
use teksilo_core::color_prop::ColorProp;
use teksilo_core::event::EventResponse;
use teksilo_core::signal::Signal;
use teksilo_core::widget::{
CursorIcon, EventContext, LayoutContext, PaintContext, Widget, WidgetPlacement,
};
use teksilo_core::widget_builder::HandlerSet;
use teksilo_core::widget_id::WidgetId;
use teksilo_tokens::{SurfaceRole, TextRole, TextStyleRole};
use crate::primitives::{Center, FixedSize, HStack, RectWidget, Switcher, TextWidget, ZStack};
use crate::title_bar::CloseAction;
#[derive(Debug, Clone)]
pub struct WindowControlsLayout {
pub minimize_id: WidgetId,
pub maximize_id: WidgetId,
pub close_id: WidgetId,
}
pub type ControlAction = Rc<dyn Fn(&mut EventContext)>;
pub struct ControlButton {
glyph: &'static str,
width: f32,
height: f32,
fg: ColorProp,
hover_role: SurfaceRole,
action: Option<ControlAction>,
a11y_name: Signal<String>,
external_hover: Option<Signal<bool>>,
root_child_id: Option<WidgetId>,
}
impl std::fmt::Debug for ControlButton {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ControlButton")
.field("glyph", &self.glyph)
.field("width", &self.width)
.field("height", &self.height)
.finish_non_exhaustive()
}
}
impl ControlButton {
pub fn new(glyph: &'static str, width: f32, height: f32, fg: impl Into<ColorProp>) -> Self {
Self {
glyph,
width,
height,
fg: fg.into(),
hover_role: SurfaceRole::Transparent,
action: None,
a11y_name: Signal::new(String::new()),
external_hover: None,
root_child_id: None,
}
}
pub(crate) fn external_hover(mut self, signal: Signal<bool>) -> Self {
self.external_hover = Some(signal);
self
}
pub fn hover_background(mut self, role: SurfaceRole) -> Self {
self.hover_role = role;
self
}
pub fn on_tap(mut self, action: impl Fn(&mut EventContext) + 'static) -> Self {
self.action = Some(Rc::new(action));
self
}
fn with_action(mut self, action: ControlAction) -> Self {
self.action = Some(action);
self
}
pub(crate) fn set_a11y_name(mut self, name: Signal<String>) -> Self {
self.a11y_name = name;
self
}
}
impl Widget for ControlButton {
fn build(&mut self, ctx: &mut teksilo_core::build_context::BuildContext) -> Vec<WidgetId> {
let hovered = ctx.signal(false);
let hover_role = self.hover_role;
let bg_role = hovered.map(move |inside| {
if *inside {
hover_role
} else {
SurfaceRole::Transparent
}
});
let bg_rect = ctx.add(RectWidget::new().background(bg_role));
let glyph_text = TextWidget::new(lit!(self.glyph))
.style(TextStyleRole::Body)
.color(self.fg.clone())
.single_line()
.a11y_hidden();
let centred_glyph = ctx.add(Center::new().child(glyph_text));
let stack = ctx.add(ZStack::new().add_child(bg_rect).add_child(centred_glyph));
let sized = ctx.add(
FixedSize::new()
.width(self.width)
.height(self.height)
.child_id(stack),
);
let hovered_handler = hovered.clone();
let mut handlers =
HandlerSet::new()
.cursor(CursorIcon::Pointer)
.on_hover(move |entered, _ctx| {
hovered_handler.set(entered);
});
if let Some(action) = self.action.take() {
let access_action = action.clone();
handlers = handlers
.on_tap(move |_pos, ctx| action(ctx))
.on_access_action(move |a, ctx: &mut EventContext| {
if a == teksilo_core::accesskit::Action::Click {
access_action(ctx);
EventResponse::Handled
} else {
EventResponse::Ignored
}
});
}
ctx.apply_self_handlers(handlers);
if let Some(ext) = self.external_hover.take() {
let hovered_ext = hovered.clone();
ctx.effect(&ext, move |entered| {
hovered_ext.set(*entered);
});
}
let self_id = ctx.self_id();
self.a11y_name.bind_to(
self_id,
ctx.binding_registry(),
teksilo_core::binding::BindingLevel::AccessibilityOnly,
);
self.root_child_id = Some(sized);
vec![sized]
}
fn layout_response(
&self,
_proposal: SizeProposal,
_ctx: &LayoutContext,
) -> teksilo_core::widget::LayoutResponse {
Size::new(self.width, self.height).into()
}
fn place_children(
&self,
bounds: Rect,
_proposal: SizeProposal,
children: &mut [WidgetPlacement],
_ctx: &LayoutContext,
) {
for child in children.iter_mut() {
child.origin = bounds.origin();
child.size = bounds.size();
}
}
fn paint(&self, _bounds: Rect, _canvas: &mut teksilo_canvas::Canvas, _ctx: &PaintContext) {}
fn accessibility(&self, builder: &mut AccessNodeBuilder) {
builder.set_role(teksilo_core::accesskit::Role::Button);
let name = self.a11y_name.get();
if !name.is_empty() {
builder.set_name(name);
}
builder.add_action(teksilo_core::accesskit::Action::Click);
}
fn children(&self) -> Vec<WidgetId> {
self.root_child_id.into_iter().collect()
}
}
pub struct WindowControls {
host: Rc<dyn PlatformTitleBarHost>,
show_restore: Signal<bool>,
close_action: Option<CloseAction>,
root_child_id: Option<WidgetId>,
layout_sink: Option<Rc<Cell<Option<WindowControlsLayout>>>>,
}
impl std::fmt::Debug for WindowControls {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("WindowControls").finish_non_exhaustive()
}
}
impl WindowControls {
pub fn new(
host: Rc<dyn PlatformTitleBarHost>,
show_restore: Signal<bool>,
close_action: Option<CloseAction>,
) -> Self {
Self {
host,
show_restore,
close_action,
root_child_id: None,
layout_sink: None,
}
}
pub(crate) fn layout_sink(mut self, sink: Rc<Cell<Option<WindowControlsLayout>>>) -> Self {
self.layout_sink = Some(sink);
self
}
}
impl Widget for WindowControls {
fn build(&mut self, ctx: &mut teksilo_core::build_context::BuildContext) -> Vec<WidgetId> {
let fg = TextRole::Primary;
let hover_bg = SurfaceRole::Hover;
let close_hover = SurfaceRole::StatusError;
let cell_w = 46.0;
let cell_h = 32.0;
let close_override = self.close_action.clone();
let minimize_action: ControlAction = Rc::new(move |ctx| {
if let Some(w) = ctx.window() {
w.placement().set(teksilo_core::WindowPlacement::Minimized);
}
});
let maximize_action: ControlAction = Rc::new(move |ctx| {
if let Some(w) = ctx.window() {
use teksilo_core::WindowPlacement as P;
let next = match w.placement().get() {
P::Maximized | P::Fullscreen => P::Floating,
P::Floating | P::Minimized => P::Maximized,
};
w.placement().set(next);
}
});
let close_action: ControlAction = match close_override {
Some(user_action) => user_action,
None => Rc::new(move |ctx| ctx.close_window()),
};
let minimize_name = teksilo_i18n::tr_widget!(a11y_window_minimize_name()).to_signal();
let close_name = teksilo_i18n::tr_widget!(a11y_window_close_name()).to_signal();
let maximize_name = teksilo_i18n::tr_widget!(a11y_window_maximize_name()).to_signal();
let restore_name = teksilo_i18n::tr_widget!(a11y_window_restore_name()).to_signal();
let minimize_hover = Signal::new(false);
let maximize_hover = Signal::new(false);
let close_hover_signal = Signal::new(false);
self.host.register_hover_signal(
teksilo_core::ControlTarget::Minimize,
minimize_hover.clone(),
);
self.host.register_hover_signal(
teksilo_core::ControlTarget::Maximize,
maximize_hover.clone(),
);
self.host.register_hover_signal(
teksilo_core::ControlTarget::Close,
close_hover_signal.clone(),
);
let minimize = ControlButton::new("\u{2014}", cell_w, cell_h, fg)
.hover_background(hover_bg)
.with_action(minimize_action)
.set_a11y_name(minimize_name)
.external_hover(minimize_hover);
let minimize_id = ctx.add(minimize);
let switcher_idx = self.show_restore.map(|b| if *b { 1usize } else { 0usize });
let maximize_action_restore = maximize_action.clone();
let maximize_normal = ControlButton::new("\u{25A1}", cell_w, cell_h, fg)
.hover_background(hover_bg)
.with_action(maximize_action)
.set_a11y_name(maximize_name)
.external_hover(maximize_hover.clone());
let maximize_zoomed = ControlButton::new("\u{25A1}", cell_w, cell_h, fg)
.hover_background(hover_bg)
.with_action(maximize_action_restore)
.set_a11y_name(restore_name)
.external_hover(maximize_hover);
let max_normal_id = ctx.add(maximize_normal);
let max_zoomed_id = ctx.add(maximize_zoomed);
let maximize_switcher = Switcher::new(switcher_idx)
.child_id(max_normal_id)
.child_id(max_zoomed_id);
let switcher_id = ctx.add(maximize_switcher);
let close = ControlButton::new("\u{00D7}", cell_w, cell_h, fg)
.hover_background(close_hover)
.with_action(close_action)
.set_a11y_name(close_name)
.external_hover(close_hover_signal);
let close_id = ctx.add(close);
let row = HStack::new()
.spacing(0.0)
.add_child(minimize_id)
.add_child(switcher_id)
.add_child(close_id);
let root = ctx.add(row);
self.root_child_id = Some(root);
if let Some(sink) = &self.layout_sink {
sink.set(Some(WindowControlsLayout {
minimize_id,
maximize_id: switcher_id,
close_id,
}));
}
vec![root]
}
fn accessibility(&self, builder: &mut AccessNodeBuilder) {
builder.set_role(teksilo_core::accesskit::Role::Group);
builder.set_name(teksilo_i18n::tr_widget!(a11y_window_controls_name()).resolve_now());
}
fn layout_response(
&self,
proposal: SizeProposal,
ctx: &LayoutContext,
) -> teksilo_core::widget::LayoutResponse {
match self.root_child_id {
Some(root_id) => ctx
.child_size(root_id, proposal)
.unwrap_or_else(|| proposal.resolve(0.0, 0.0)),
None => proposal.resolve(0.0, 0.0),
}
.into()
}
fn place_children(
&self,
bounds: Rect,
_proposal: SizeProposal,
children: &mut [WidgetPlacement],
_ctx: &LayoutContext,
) {
for child in children.iter_mut() {
child.origin = bounds.origin();
child.size = bounds.size();
}
}
fn paint(&self, _bounds: Rect, _canvas: &mut teksilo_canvas::Canvas, _ctx: &PaintContext) {}
fn children(&self) -> Vec<WidgetId> {
self.root_child_id.into_iter().collect()
}
}