use crate::kurbo::{Line, Point, Rect, Size};
use crate::widget::flex::Axis;
use crate::{
theme, BoxConstraints, Color, Cursor, Data, Env, Event, EventCtx, LayoutCtx, LifeCycle,
LifeCycleCtx, PaintCtx, RenderContext, UpdateCtx, Widget, WidgetPod,
};
pub struct Split<T> {
split_axis: Axis,
split_point_chosen: f64,
split_point_effective: f64,
min_size: f64, bar_size: f64, min_bar_area: f64, solid: bool,
draggable: bool,
child1: WidgetPod<T, Box<dyn Widget<T>>>,
child2: WidgetPod<T, Box<dyn Widget<T>>>,
}
impl<T> Split<T> {
fn new(
split_axis: Axis,
child1: impl Widget<T> + 'static,
child2: impl Widget<T> + 'static,
) -> Self {
Split {
split_axis,
split_point_chosen: 0.5,
split_point_effective: 0.5,
min_size: 0.0,
bar_size: 6.0,
min_bar_area: 6.0,
solid: false,
draggable: false,
child1: WidgetPod::new(child1).boxed(),
child2: WidgetPod::new(child2).boxed(),
}
}
pub fn columns(child1: impl Widget<T> + 'static, child2: impl Widget<T> + 'static) -> Self {
Self::new(Axis::Horizontal, child1, child2)
}
pub fn rows(child1: impl Widget<T> + 'static, child2: impl Widget<T> + 'static) -> Self {
Self::new(Axis::Vertical, child1, child2)
}
pub fn split_point(mut self, split_point: f64) -> Self {
assert!(
split_point >= 0.0 && split_point <= 1.0,
"split_point must be in the range [0.0-1.0]!"
);
self.split_point_chosen = split_point;
self
}
pub fn min_size(mut self, min_size: f64) -> Self {
assert!(min_size >= 0.0);
self.min_size = min_size.ceil();
self
}
pub fn bar_size(mut self, bar_size: f64) -> Self {
assert!(bar_size >= 0.0, "bar_size must be 0.0 or greater!");
self.bar_size = bar_size.ceil();
self
}
pub fn min_bar_area(mut self, min_bar_area: f64) -> Self {
assert!(min_bar_area >= 0.0, "min_bar_area must be 0.0 or greater!");
self.min_bar_area = min_bar_area.ceil();
self
}
pub fn draggable(mut self, draggable: bool) -> Self {
self.draggable = draggable;
self
}
pub fn solid_bar(mut self, solid: bool) -> Self {
self.solid = solid;
self
}
#[inline]
fn bar_area(&self) -> f64 {
self.bar_size.max(self.min_bar_area)
}
#[inline]
fn bar_padding(&self) -> f64 {
(self.bar_area() - self.bar_size) / 2.0
}
fn bar_edges(&self, size: Size) -> (f64, f64) {
let bar_area = self.bar_area();
match self.split_axis {
Axis::Horizontal => {
let reduced_width = size.width - bar_area;
let edge1 = (reduced_width * self.split_point_effective).floor();
let edge2 = edge1 + bar_area;
(edge1, edge2)
}
Axis::Vertical => {
let reduced_height = size.height - bar_area;
let edge1 = (reduced_height * self.split_point_effective).floor();
let edge2 = edge1 + bar_area;
(edge1, edge2)
}
}
}
fn bar_hit_test(&self, size: Size, mouse_pos: Point) -> bool {
let (edge1, edge2) = self.bar_edges(size);
match self.split_axis {
Axis::Horizontal => mouse_pos.x >= edge1 && mouse_pos.x <= edge2,
Axis::Vertical => mouse_pos.y >= edge1 && mouse_pos.y <= edge2,
}
}
fn split_side_limits(&self, size: Size) -> (f64, f64) {
let split_axis_size = self.split_axis.major(size);
let mut min_limit = self.min_size;
let mut max_limit = (split_axis_size - min_limit).max(0.0);
if min_limit > max_limit {
min_limit = 0.5 * (min_limit + max_limit);
max_limit = min_limit;
}
(min_limit, max_limit)
}
fn update_split_point(&mut self, size: Size, mouse_pos: Point) {
let (min_limit, max_limit) = self.split_side_limits(size);
self.split_point_chosen = match self.split_axis {
Axis::Horizontal => clamp(mouse_pos.x, min_limit, max_limit) / size.width,
Axis::Vertical => clamp(mouse_pos.y, min_limit, max_limit) / size.height,
}
}
fn bar_color(&self, env: &Env) -> Color {
if self.draggable {
env.get(theme::BORDER_LIGHT)
} else {
env.get(theme::BORDER_DARK)
}
}
fn paint_solid_bar(&mut self, ctx: &mut PaintCtx, env: &Env) {
let size = ctx.size();
let (edge1, edge2) = self.bar_edges(size);
let padding = self.bar_padding();
let rect = match self.split_axis {
Axis::Horizontal => Rect::from_points(
Point::new(edge1 + padding.ceil(), 0.0),
Point::new(edge2 - padding.floor(), size.height),
),
Axis::Vertical => Rect::from_points(
Point::new(0.0, edge1 + padding.ceil()),
Point::new(size.width, edge2 - padding.floor()),
),
};
let splitter_color = self.bar_color(env);
ctx.fill(rect, &splitter_color);
}
fn paint_stroked_bar(&mut self, ctx: &mut PaintCtx, env: &Env) {
let size = ctx.size();
let line_width = (self.bar_size / 3.0).floor();
let line_midpoint = line_width / 2.0;
let (edge1, edge2) = self.bar_edges(size);
let padding = self.bar_padding();
let (line1, line2) = match self.split_axis {
Axis::Horizontal => (
Line::new(
Point::new(edge1 + line_midpoint + padding.ceil(), 0.0),
Point::new(edge1 + line_midpoint + padding.ceil(), size.height),
),
Line::new(
Point::new(edge2 - line_midpoint - padding.floor(), 0.0),
Point::new(edge2 - line_midpoint - padding.floor(), size.height),
),
),
Axis::Vertical => (
Line::new(
Point::new(0.0, edge1 + line_midpoint + padding.ceil()),
Point::new(size.width, edge1 + line_midpoint + padding.ceil()),
),
Line::new(
Point::new(0.0, edge2 - line_midpoint - padding.floor()),
Point::new(size.width, edge2 - line_midpoint - padding.floor()),
),
),
};
let splitter_color = self.bar_color(env);
ctx.stroke(line1, &splitter_color, line_width);
ctx.stroke(line2, &splitter_color, line_width);
}
}
impl<T: Data> Widget<T> for Split<T> {
fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut T, env: &Env) {
if self.child1.is_active() {
self.child1.event(ctx, event, data, env);
if ctx.is_handled() {
return;
}
}
if self.child2.is_active() {
self.child2.event(ctx, event, data, env);
if ctx.is_handled() {
return;
}
}
if self.draggable {
match event {
Event::MouseDown(mouse) => {
if mouse.button.is_left() && self.bar_hit_test(ctx.size(), mouse.pos) {
ctx.set_active(true);
ctx.set_handled();
}
}
Event::MouseUp(mouse) => {
if mouse.button.is_left() && ctx.is_active() {
ctx.set_active(false);
self.update_split_point(ctx.size(), mouse.pos);
ctx.request_paint();
}
}
Event::MouseMove(mouse) => {
if ctx.is_active() {
self.update_split_point(ctx.size(), mouse.pos);
ctx.request_layout();
}
if ctx.is_hot() && self.bar_hit_test(ctx.size(), mouse.pos) || ctx.is_active() {
match self.split_axis {
Axis::Horizontal => ctx.set_cursor(&Cursor::ResizeLeftRight),
Axis::Vertical => ctx.set_cursor(&Cursor::ResizeUpDown),
};
}
}
_ => {}
}
}
if !self.child1.is_active() {
self.child1.event(ctx, event, data, env);
}
if !self.child2.is_active() {
self.child2.event(ctx, event, data, env);
}
}
fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, data: &T, env: &Env) {
self.child1.lifecycle(ctx, event, data, env);
self.child2.lifecycle(ctx, event, data, env);
}
fn update(&mut self, ctx: &mut UpdateCtx, _old_data: &T, data: &T, env: &Env) {
self.child1.update(ctx, &data, env);
self.child2.update(ctx, &data, env);
}
fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints, data: &T, env: &Env) -> Size {
bc.debug_check("Split");
match self.split_axis {
Axis::Horizontal => {
if !bc.is_width_bounded() {
log::warn!("A Split widget was given an unbounded width to split.")
}
}
Axis::Vertical => {
if !bc.is_height_bounded() {
log::warn!("A Split widget was given an unbounded height to split.")
}
}
}
let mut my_size = bc.max();
let bar_area = self.bar_area();
let reduced_size = Size::new(
(my_size.width - bar_area).max(0.),
(my_size.height - bar_area).max(0.),
);
self.split_point_effective = {
let (min_limit, max_limit) = self.split_side_limits(reduced_size);
let reduced_axis_size = self.split_axis.major(reduced_size);
if reduced_axis_size.is_infinite() || reduced_axis_size <= std::f64::EPSILON {
0.5
} else {
clamp(
self.split_point_chosen,
min_limit / reduced_axis_size,
max_limit / reduced_axis_size,
)
}
};
let (child1_bc, child2_bc) = match self.split_axis {
Axis::Horizontal => {
let child1_width = (reduced_size.width * self.split_point_effective)
.floor()
.max(0.0);
let child2_width = (reduced_size.width - child1_width).max(0.0);
(
BoxConstraints::new(
Size::new(child1_width, bc.min().height),
Size::new(child1_width, bc.max().height),
),
BoxConstraints::new(
Size::new(child2_width, bc.min().height),
Size::new(child2_width, bc.max().height),
),
)
}
Axis::Vertical => {
let child1_height = (reduced_size.height * self.split_point_effective)
.floor()
.max(0.0);
let child2_height = (reduced_size.height - child1_height).max(0.0);
(
BoxConstraints::new(
Size::new(bc.min().width, child1_height),
Size::new(bc.max().width, child1_height),
),
BoxConstraints::new(
Size::new(bc.min().width, child2_height),
Size::new(bc.max().width, child2_height),
),
)
}
};
let child1_size = self.child1.layout(ctx, &child1_bc, &data, env);
let child2_size = self.child2.layout(ctx, &child2_bc, &data, env);
let (child1_rect, child2_rect) = match self.split_axis {
Axis::Horizontal => {
my_size.height = child1_size.height.max(child2_size.height);
(
Rect::from_origin_size(Point::ORIGIN, child1_size),
Rect::from_origin_size(
Point::new(child1_size.width + bar_area, 0.0),
child2_size,
),
)
}
Axis::Vertical => {
my_size.width = child1_size.width.max(child2_size.width);
(
Rect::from_origin_size(Point::ORIGIN, child1_size),
Rect::from_origin_size(
Point::new(0.0, child1_size.height + bar_area),
child2_size,
),
)
}
};
self.child1.set_layout_rect(ctx, data, env, child1_rect);
self.child2.set_layout_rect(ctx, data, env, child2_rect);
let paint_rect = self.child1.paint_rect().union(self.child2.paint_rect());
let insets = paint_rect - Rect::ZERO.with_size(my_size);
ctx.set_paint_insets(insets);
my_size
}
fn paint(&mut self, ctx: &mut PaintCtx, data: &T, env: &Env) {
if self.solid {
self.paint_solid_bar(ctx, env);
} else {
self.paint_stroked_bar(ctx, env);
}
self.child1.paint(ctx, &data, env);
self.child2.paint(ctx, &data, env);
}
}
fn clamp(mut x: f64, min: f64, max: f64) -> f64 {
assert!(min <= max);
if x < min {
x = min;
}
if x > max {
x = max;
}
x
}