use crate::event::{Event, EventCtx};
use crate::geometry::{Color, Point, Rect, Size};
use crate::painter::Painter;
use crate::theme::Theme;
use crate::widget::{PopupRequest, Widget};
use crate::widgets::{TabAction, tab_action};
pub struct Container {
pub size: Size,
pub background: Option<Color>,
pub border: Option<Color>,
children: Vec<Box<dyn Widget>>,
captured: Option<usize>,
focused: Option<usize>,
origin: Point,
}
impl Container {
pub fn new(width: i32, height: i32) -> Self {
Self {
size: Size::new(width, height),
background: None,
border: None,
children: Vec::new(),
captured: None,
focused: None,
origin: Point::new(0, 0),
}
}
pub fn with_background(mut self, color: Color) -> Self {
self.background = Some(color);
self
}
pub fn with_border(mut self, color: Color) -> Self {
self.border = Some(color);
self
}
#[allow(clippy::should_implement_trait)]
pub fn add(mut self, widget: impl Widget + 'static) -> Self {
self.push(widget);
self
}
pub fn push(&mut self, widget: impl Widget + 'static) {
self.children.push(Box::new(widget));
}
pub fn focused_index(&self) -> Option<usize> {
self.focused
}
pub fn focus_child(&mut self, index: usize) -> bool {
if self.children.get(index).map(|c| c.focusable()) != Some(true) {
return false;
}
if let Some(old) = self.focused
&& old != index
&& let Some(c) = self.children.get_mut(old)
{
c.set_focused(false);
}
let focused = self.children[index].focus_first();
if focused {
self.focused = Some(index);
}
focused
}
fn choose_target(&self, event: &Event) -> Option<usize> {
if event.is_keyboard() {
return self.focused;
}
if let Some(idx) = self.captured {
return Some(idx);
}
if let Some(idx) = self
.children
.iter()
.position(|c| c.accepts_accelerators() && c.captures_pointer())
{
return Some(idx);
}
let pos = event.position()?;
(0..self.children.len())
.rev()
.find(|&i| self.children[i].bounds().contains(pos))
}
fn change_focus(&mut self, new_focus: Option<usize>, ctx: &mut EventCtx) {
if new_focus == self.focused {
return;
}
if let Some(old) = self.focused
&& let Some(child) = self.children.get_mut(old)
{
child.set_focused(false);
}
if let Some(new) = new_focus
&& let Some(child) = self.children.get_mut(new)
{
child.focus_first();
}
self.focused = new_focus;
ctx.request_paint();
}
fn focusable_count(&self) -> usize {
self.children.iter().filter(|c| c.focusable()).count()
}
fn cycle_focus(&mut self, dir: i32, ctx: &mut EventCtx) -> bool {
let n = self.children.len();
if n == 0 {
return false;
}
let candidates: Vec<usize> = (0..n).filter(|&i| self.children[i].focusable()).collect();
if candidates.is_empty() {
return false;
}
let next = next_in_cycle(&candidates, self.focused, dir);
if Some(next) == self.focused {
return false;
}
self.change_focus(Some(next), ctx);
true
}
}
fn next_in_cycle(candidates: &[usize], current: Option<usize>, dir: i32) -> usize {
let n = candidates.len() as i32;
let cur_pos = current.and_then(|c| candidates.iter().position(|&i| i == c));
match cur_pos {
None => {
if dir > 0 {
candidates[0]
} else {
candidates[(n - 1) as usize]
}
}
Some(p) => {
let np = ((p as i32) + dir).rem_euclid(n) as usize;
candidates[np]
}
}
}
impl Widget for Container {
fn bounds(&self) -> Rect {
Rect::new(self.origin.x, self.origin.y, self.size.w, self.size.h)
}
fn layout(&mut self, bounds: Rect) {
let dx = bounds.x - self.origin.x;
let dy = bounds.y - self.origin.y;
if dx != 0 || dy != 0 {
for child in &mut self.children {
let b = child.bounds();
child.layout(Rect::new(b.x + dx, b.y + dy, b.w, b.h));
}
}
self.origin = Point::new(bounds.x, bounds.y);
}
fn paint(&mut self, painter: &mut Painter, theme: &Theme) {
if let Some(bg) = self.background {
painter.fill_rect(self.bounds(), bg);
}
for child in &mut self.children {
child.paint(painter, theme);
}
if let Some(border) = self.border {
painter.stroke_rect(self.bounds(), border);
}
for child in &mut self.children {
child.paint_overlay(painter, theme);
}
}
fn paint_overlay(&mut self, painter: &mut Painter, theme: &Theme) {
for child in &mut self.children {
child.paint_overlay(painter, theme);
}
}
fn event(&mut self, event: &Event, ctx: &mut EventCtx) {
if !event.is_keyboard() && event.position().is_none() && self.captured.is_none() {
for child in &mut self.children {
child.event(event, ctx);
}
return;
}
let focused_capturing = self
.focused
.and_then(|i| self.children.get(i))
.is_some_and(|c| c.captures_pointer());
if event.is_keyboard() && !focused_capturing {
let mut accelerator_blocking = false;
for (idx, child) in self.children.iter_mut().enumerate() {
if child.accepts_accelerators() && Some(idx) != self.focused {
child.event(event, ctx);
if ctx.is_consumed() {
return;
}
if child.captures_pointer() {
accelerator_blocking = true;
}
}
}
if accelerator_blocking {
return;
}
match tab_action(event) {
Some(TabAction::Cycle(dir)) if self.cycle_focus(dir, ctx) => {
return;
}
Some(TabAction::Swallow) if self.focusable_count() >= 2 => return,
_ => {}
}
}
let Some(idx) = self.choose_target(event) else {
return;
};
let captured_was_set = self.captured == Some(idx);
{
let child = &mut self.children[idx];
child.event(event, ctx);
if !event.is_keyboard() {
if child.captures_pointer() {
self.captured = Some(idx);
} else if captured_was_set {
self.captured = None;
}
}
}
if ctx.focus_requested {
ctx.focus_requested = false;
self.change_focus(Some(idx), ctx);
}
if ctx.focus_released {
ctx.focus_released = false;
if self.focused == Some(idx) {
self.change_focus(None, ctx);
}
}
}
fn captures_pointer(&self) -> bool {
self.captured.is_some()
}
fn focusable(&self) -> bool {
self.children.iter().any(|c| c.focusable())
}
fn focus_first(&mut self) -> bool {
for (idx, child) in self.children.iter_mut().enumerate() {
if child.focus_first() {
self.focused = Some(idx);
return true;
}
}
false
}
fn popup_request(&self) -> Option<PopupRequest> {
for child in &self.children {
if let Some(req) = child.popup_request() {
return Some(req);
}
}
None
}
fn collect_popups(&self, out: &mut Vec<PopupRequest>) {
for child in &self.children {
child.collect_popups(out);
}
}
fn wants_ticks(&self) -> bool {
self.children.iter().any(|c| c.wants_ticks())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::event::MouseButton;
use crate::geometry::Point;
use std::cell::Cell;
use std::rc::Rc;
struct Probe {
rect: Rect,
hit: Rc<Cell<bool>>,
}
impl Widget for Probe {
fn bounds(&self) -> Rect {
self.rect
}
fn layout(&mut self, bounds: Rect) {
self.rect = bounds;
}
fn paint(&mut self, _: &mut Painter, _: &Theme) {}
fn event(&mut self, event: &Event, ctx: &mut EventCtx) {
if let Event::PointerDown { .. } = event {
self.hit.set(true);
ctx.request_focus();
}
}
fn focusable(&self) -> bool {
true
}
}
fn press(c: &mut Container, x: i32, y: i32) {
let mut ctx = EventCtx::new();
c.event(
&Event::PointerDown {
pos: Point::new(x, y),
button: MouseButton::Left,
},
&mut ctx,
);
}
#[test]
fn layout_shifts_children_to_the_container_origin() {
let hit = Rc::new(Cell::new(false));
let mut c = Container::new(100, 50).add(Probe {
rect: Rect::new(10, 8, 20, 12),
hit: hit.clone(),
});
c.layout(Rect::new(200, 300, 100, 50));
assert_eq!(c.bounds(), Rect::new(200, 300, 100, 50));
press(&mut c, 15, 12);
assert!(
!hit.get(),
"child must have moved off its authored position"
);
press(&mut c, 215, 312);
assert!(
hit.get(),
"child should be hit at its origin-shifted position"
);
assert_eq!(c.focused_index(), Some(0));
}
#[test]
fn layout_at_origin_is_a_no_op() {
let hit = Rc::new(Cell::new(false));
let mut c = Container::new(100, 50).add(Probe {
rect: Rect::new(10, 8, 20, 12),
hit: hit.clone(),
});
c.layout(Rect::new(0, 0, 100, 50));
press(&mut c, 15, 12);
assert!(hit.get(), "at the origin the children must not move");
}
}