use teksilo_canvas::{Canvas, Point, Rect, Size, SizeProposal};
use teksilo_core::accessibility::AccessNodeBuilder;
use teksilo_core::accesskit::Role;
use teksilo_core::binding::BindingLevel;
use teksilo_core::build_context::BuildContext;
use teksilo_core::color_prop::ColorProp;
use teksilo_core::signal::Signal;
use teksilo_core::styles::{DropRegion, region_rect};
use teksilo_core::widget::{LayoutContext, LayoutResponse, PaintContext, Widget, WidgetPlacement};
use teksilo_core::widget_builder::HandlerSet;
use teksilo_core::widget_id::WidgetId;
use teksilo_tokens::{BorderRole, SurfaceRole};
const REGION_FILL_ALPHA: f32 = 0.22;
pub(crate) struct DropRegionOverlay {
active_region: Signal<Option<DropRegion>>,
size_factor: f32,
border_width: f32,
hints: Vec<(DropRegion, WidgetId)>,
fill: ColorProp,
border: ColorProp,
}
impl std::fmt::Debug for DropRegionOverlay {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DropRegionOverlay")
.field("size_factor", &self.size_factor)
.field("border_width", &self.border_width)
.field("hint_count", &self.hints.len())
.finish()
}
}
impl DropRegionOverlay {
pub(crate) fn new(
active_region: Signal<Option<DropRegion>>,
size_factor: f32,
border_width: f32,
hints: Vec<(DropRegion, WidgetId)>,
) -> Self {
Self {
active_region,
size_factor,
border_width,
hints,
fill: ColorProp::from(SurfaceRole::Accent),
border: ColorProp::from(BorderRole::Accent),
}
}
}
impl Widget for DropRegionOverlay {
fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
self.active_region.bind_to(
ctx.self_id(),
ctx.binding_registry(),
BindingLevel::RepaintOnly,
);
ctx.apply_self_handlers(HandlerSet::new().event_pass_through(true));
self.hints.iter().map(|(_, id)| *id).collect()
}
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 child in children.iter_mut() {
let region = self
.hints
.iter()
.find(|(_, id)| *id == child.id)
.map(|(r, _)| *r)
.unwrap_or(DropRegion::Center);
let zone = region_rect(region, bounds, self.size_factor);
let natural = ctx
.child_size(child.id, SizeProposal::unspecified())
.unwrap_or(zone.size());
let child_size = Size::new(
natural.width.min(zone.width),
natural.height.min(zone.height),
);
let dx = ((zone.width - child_size.width) / 2.0).max(0.0);
let dy = ((zone.height - child_size.height) / 2.0).max(0.0);
child.origin = Point::new(zone.x + dx, zone.y + dy);
child.size = child_size;
}
}
fn paint(&self, bounds: Rect, canvas: &mut Canvas, ctx: &PaintContext) {
if self.border_width <= 0.0 {
return;
}
let Some(region) = self.active_region.get() else {
return;
};
if !region.is_side() {
return;
}
let rect = region_rect(region, bounds, self.size_factor);
let fill = self
.fill
.resolve(ctx.theme, true)
.with_alpha(REGION_FILL_ALPHA);
canvas.fill_rect(rect, fill);
let border = self.border.resolve(ctx.theme, true);
let t = self.border_width;
canvas.fill_rect(Rect::new(rect.x, rect.y, rect.width, t), border);
canvas.fill_rect(
Rect::new(rect.x, rect.y + rect.height - t, rect.width, t),
border,
);
canvas.fill_rect(Rect::new(rect.x, rect.y, t, rect.height), border);
canvas.fill_rect(
Rect::new(rect.x + rect.width - t, rect.y, t, rect.height),
border,
);
}
fn accessibility(&self, builder: &mut AccessNodeBuilder) {
if self.hints.is_empty() {
builder.set_hidden();
} else {
builder.set_role(Role::GenericContainer);
}
}
fn children(&self) -> Vec<WidgetId> {
self.hints.iter().map(|(_, id)| *id).collect()
}
}