use std::rc::Rc;
use teksilo_canvas::{Canvas, Point, Rect, SizeProposal};
use crate::accessibility::AccessNodeBuilder;
use crate::binding::BindingLevel;
use crate::build_context::BuildContext;
use crate::event::{EventResponse, WidgetEvent};
use crate::overlay::SelectionHandleKind;
use crate::styles::{TextMagnifierRecipe, TextSelectionHandleRecipe};
use crate::widget::{
EventContext, LayoutContext, LayoutResponse, PaintContext, Widget, WidgetPlacement,
};
use crate::widget_builder::WidgetBuilder;
use crate::widget_id::WidgetId;
use super::{HandleDragPhase, SelectionHandleGeometry, TextAffordances};
pub trait TextAffordanceDelegate {
fn handle_drag(
&self,
kind: SelectionHandleKind,
phase: HandleDragPhase,
point: Point,
ctx: &mut EventContext<'_>,
);
fn set_handle_offset(
&self,
kind: SelectionHandleKind,
offset: usize,
ctx: &mut EventContext<'_>,
);
}
pub struct SelectionHandle {
kind: SelectionHandleKind,
affordances: TextAffordances,
recipe: TextSelectionHandleRecipe,
delegate: Rc<dyn TextAffordanceDelegate>,
}
impl std::fmt::Debug for SelectionHandle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SelectionHandle")
.field("kind", &self.kind)
.finish()
}
}
impl SelectionHandle {
pub fn new(
kind: SelectionHandleKind,
affordances: TextAffordances,
recipe: TextSelectionHandleRecipe,
delegate: Rc<dyn TextAffordanceDelegate>,
) -> Self {
Self {
kind,
affordances,
recipe,
delegate,
}
}
fn geometry(&self) -> Option<SelectionHandleGeometry> {
self.affordances.handle(self.kind)
}
fn default_label(&self) -> &'static str {
match self.kind {
SelectionHandleKind::Caret => "Text cursor",
SelectionHandleKind::Start => "Selection start",
SelectionHandleKind::End => "Selection end",
}
}
}
impl Widget for SelectionHandle {
fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
let kind = self.kind;
let drag_delegate = Rc::clone(&self.delegate);
let action_delegate = Rc::clone(&self.delegate);
let handlers = crate::widget_builder::HandlerSet::new()
.on_pointer_event(move |event, ctx| {
if !ctx.pointer_kind().is_direct() {
return EventResponse::Ignored;
}
match event {
WidgetEvent::PointerDown { position, .. } => {
ctx.capture_pointer();
drag_delegate.handle_drag(kind, HandleDragPhase::Begin, *position, ctx);
EventResponse::Handled
}
WidgetEvent::PointerMove { position, .. } => {
drag_delegate.handle_drag(kind, HandleDragPhase::Move, *position, ctx);
EventResponse::Handled
}
WidgetEvent::PointerUp { position, .. } => {
drag_delegate.handle_drag(kind, HandleDragPhase::End, *position, ctx);
EventResponse::Handled
}
WidgetEvent::PointerCancel { .. } => {
drag_delegate.handle_drag(
kind,
HandleDragPhase::Cancel,
Point::new(0.0, 0.0),
ctx,
);
EventResponse::Handled
}
_ => EventResponse::Ignored,
}
})
.on_access_action_request(move |action, _node, data, ctx| {
if action != accesskit::Action::SetValue {
return EventResponse::Ignored;
}
let offset = match data {
Some(accesskit::ActionData::NumericValue(v)) => v.max(0.0) as usize,
Some(accesskit::ActionData::Value(v)) => match v.parse::<usize>() {
Ok(parsed) => parsed,
Err(_) => return EventResponse::Ignored,
},
_ => return EventResponse::Ignored,
};
action_delegate.set_handle_offset(kind, offset, ctx);
EventResponse::Handled
});
ctx.apply_self_handlers(handlers);
vec![]
}
fn layout_response(&self, proposal: SizeProposal, _ctx: &LayoutContext) -> LayoutResponse {
let extent = self.recipe.hit_size;
proposal.resolve(extent, extent).into()
}
fn paint(&self, bounds: Rect, canvas: &mut Canvas, ctx: &PaintContext) {
let Some(geometry) = self.geometry() else {
return;
};
let fill = self.recipe.fill.resolve(ctx.theme);
let radius = self.recipe.diameter / 2.0;
let centre = Point::new(
geometry
.anchor
.x
.clamp(bounds.x + radius, bounds.right() - radius),
geometry
.anchor
.y
.clamp(bounds.y + radius, bounds.bottom() - radius),
);
if self.recipe.stem_width > 0.0 {
let stem_x = centre.x - self.recipe.stem_width / 2.0;
let caret = geometry.caret;
let (top, bottom) = if centre.y < caret.y {
(centre.y, caret.y)
} else {
(caret.bottom(), centre.y)
};
if bottom > top {
canvas.fill_rect(
Rect::new(stem_x, top, self.recipe.stem_width, bottom - top),
fill,
);
}
}
if self.recipe.outline_width > 0.0 {
canvas.stroke_circle(
centre,
radius,
self.recipe.outline.resolve(ctx.theme),
self.recipe.outline_width,
);
}
canvas.fill_circle(centre, radius, fill);
}
fn accessibility(&self, builder: &mut AccessNodeBuilder) {
builder.set_role(accesskit::Role::Slider);
builder.set_name(self.default_label());
if let Some(geometry) = self.geometry() {
builder.set_numeric_value(geometry.offset as f64);
builder.set_min_numeric_value(0.0);
builder.set_max_numeric_value(geometry.document_len as f64);
builder.set_numeric_value_step(1.0);
}
builder.add_action(accesskit::Action::SetValue);
}
}
pub struct TextMagnifier {
affordances: TextAffordances,
recipe: TextMagnifierRecipe,
painter: Rc<dyn Fn(&mut Canvas, &PaintContext<'_>)>,
}
impl std::fmt::Debug for TextMagnifier {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TextMagnifier").finish()
}
}
impl TextMagnifier {
pub fn new(
affordances: TextAffordances,
recipe: TextMagnifierRecipe,
painter: Rc<dyn Fn(&mut Canvas, &PaintContext<'_>)>,
) -> Self {
Self {
affordances,
recipe,
painter,
}
}
}
impl Widget for TextMagnifier {
fn layout_response(&self, proposal: SizeProposal, _ctx: &LayoutContext) -> LayoutResponse {
proposal
.resolve(self.recipe.radius * 2.0, self.recipe.half_height * 2.0)
.into()
}
fn paint(&self, bounds: Rect, canvas: &mut Canvas, ctx: &PaintContext) {
let Some(request) = self.affordances.magnifier() else {
return;
};
let corner = self.recipe.corner_radius;
canvas.fill_rounded_rect(
bounds,
teksilo_tokens::CornerRadius::uniform(corner),
self.recipe.background.resolve(ctx.theme),
);
ctx.replay(canvas, &*self.painter, request.transform(), bounds);
if self.recipe.border_width > 0.0 {
canvas.stroke_rounded_rect(
bounds,
teksilo_tokens::CornerRadius::uniform(corner),
self.recipe.border.resolve(ctx.theme),
self.recipe.border_width,
);
}
}
fn accessibility(&self, builder: &mut AccessNodeBuilder) {
builder.set_hidden();
}
}
pub struct TextAffordanceLayer {
affordances: TextAffordances,
handle_recipe: TextSelectionHandleRecipe,
magnifier_recipe: TextMagnifierRecipe,
delegate: Rc<dyn TextAffordanceDelegate>,
painter: Option<Rc<dyn Fn(&mut Canvas, &PaintContext<'_>)>>,
handles: Vec<(SelectionHandleKind, WidgetId)>,
magnifier: Option<WidgetId>,
}
impl std::fmt::Debug for TextAffordanceLayer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TextAffordanceLayer")
.field("handles", &self.handles.len())
.field("magnifier", &self.magnifier.is_some())
.finish()
}
}
impl TextAffordanceLayer {
pub fn new(
affordances: TextAffordances,
handle_recipe: TextSelectionHandleRecipe,
magnifier_recipe: TextMagnifierRecipe,
delegate: Rc<dyn TextAffordanceDelegate>,
) -> Self {
Self {
affordances,
handle_recipe,
magnifier_recipe,
delegate,
painter: None,
handles: Vec::new(),
magnifier: None,
}
}
pub fn magnifier_painter(
mut self,
painter: Rc<dyn Fn(&mut Canvas, &PaintContext<'_>)>,
) -> Self {
self.painter = Some(painter);
self
}
}
impl Widget for TextAffordanceLayer {
fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
self.affordances.version_signal().bind_to(
ctx.self_id(),
ctx.binding_registry(),
BindingLevel::Relayout,
);
self.handles.clear();
for kind in [
SelectionHandleKind::Caret,
SelectionHandleKind::Start,
SelectionHandleKind::End,
] {
let id = ctx.add(
SelectionHandle::new(
kind,
self.affordances.clone(),
self.handle_recipe,
Rc::clone(&self.delegate),
)
.visible_when(self.affordances.handle_visible_signal(kind)),
);
self.handles.push((kind, id));
}
self.magnifier = self.painter.as_ref().map(|painter| {
ctx.add(
TextMagnifier::new(
self.affordances.clone(),
self.magnifier_recipe,
Rc::clone(painter),
)
.visible_when(self.affordances.magnifier_visible_signal()),
)
});
let handlers = crate::widget_builder::HandlerSet::new().event_pass_through(true);
ctx.apply_self_handlers(handlers);
self.children()
}
fn layout_response(&self, proposal: SizeProposal, _ctx: &LayoutContext) -> LayoutResponse {
proposal.resolve(0.0, 0.0).into()
}
fn place_children(
&self,
_bounds: Rect,
_proposal: SizeProposal,
children: &mut [WidgetPlacement],
_ctx: &LayoutContext,
) {
for placement in children.iter_mut() {
if let Some((kind, _)) = self.handles.iter().find(|(_, id)| *id == placement.id) {
if let Some(geometry) = self.affordances.handle(*kind) {
placement.origin = geometry.hit.origin();
placement.size = geometry.hit.size();
}
} else if Some(placement.id) == self.magnifier
&& let Some(request) = self.affordances.magnifier()
{
placement.origin = request.lens.origin();
placement.size = request.lens.size();
}
}
}
fn children(&self) -> Vec<WidgetId> {
self.handles
.iter()
.map(|(_, id)| *id)
.chain(self.magnifier)
.collect()
}
fn accessibility(&self, builder: &mut AccessNodeBuilder) {
builder.set_role(accesskit::Role::GenericContainer);
}
}