#include #include namespace psemek::ui { void scroller::set_preferred_direction(direction d) { preferred_direction_ = d; } bool scroller::set_horizontal_scroll(bool enabled) { std::swap(horizontal_, enabled); return enabled; } bool scroller::set_vertical_scroll(bool enabled) { std::swap(vertical_, enabled); return enabled; } math::box scroller::events_bbox() const { auto st = merged_own_style(); auto result = shape_.box; if (horizontal_scroll()) result[1].max -= width() * *st->scale; if (vertical_scroll()) result[0].max -= width() * *st->scale; return result; } bool scroller::on_event(mouse_move const & e) { mouse_ = e.position; auto pos = math::cast(e.position); if (horizontal_scroll()) { auto box = horizontal_box(); bool over = math::contains(box, pos); switch (horizontal_state_) { case state_t::normal: if (over) horizontal_state_ = state_t::mouseover; break; case state_t::mouseover: if (!over) horizontal_state_ = state_t::normal; break; case state_t::mousedown: { if (!child_) return false; float c = math::unlerp(box[0], e.position[0]); set_position(direction::horizontal, math::clamp(c, {0.f, 1.f}), true); } break; } } if (vertical_scroll()) { auto box = vertical_box(); bool over = math::contains(box, pos); switch (vertical_state_) { case state_t::normal: if (over) vertical_state_ = state_t::mouseover; break; case state_t::mouseover: if (!over) vertical_state_ = state_t::normal; break; case state_t::mousedown: { if (!child_) return false; float c = math::unlerp(box[1], e.position[1]); c = math::clamp(c, {0.f, 1.f}); set_position(direction::vertical, math::clamp(c, {0.f, 1.f}), true); } break; } } return false; } bool scroller::on_event(mouse_click const & e) { if (e.button != mouse_button::left) return false; bool result = false; if (horizontal_scroll()) { switch (horizontal_state_) { case state_t::normal: break; case state_t::mouseover: if (e.down) { horizontal_state_ = state_t::mousedown; result = true; } break; case state_t::mousedown: if (!e.down) { horizontal_state_ = state_t::mouseover; result = true; } break; } } if (vertical_scroll()) { switch (vertical_state_) { case state_t::normal: break; case state_t::mouseover: if (e.down) { vertical_state_ = state_t::mousedown; result = true; } break; case state_t::mousedown: if (!e.down) { vertical_state_ = state_t::mouseover; result = true; } break; } } return result; } bool scroller::on_event(mouse_wheel const & e) { if (mouse_ && math::contains(shape_.box, math::cast(*mouse_))) { math::vector delta = {0.f, 0.f}; auto pos = math::cast(*mouse_); auto vcontains = math::contains(vertical_box(), pos); auto hcontains = math::contains(horizontal_box(), pos); if (vertical_scroll() && horizontal_scroll()) { if (vcontains) delta[1] = e.delta; else if (hcontains) delta[0] = e.delta; else if (preferred_direction() == direction::vertical) delta[1] = e.delta; else delta[0] = e.delta; } else if (vertical_scroll()) { delta[1] = e.delta; } else if (horizontal_scroll()) { delta[0] = e.delta; } auto old = shift_tgt_; shift_tgt_ += delta * 50.f; clamp_shift(); on_scroll(shift_tgt_ - old); post_reshape(); return true; } return false; } void scroller::reshape(math::box const & bbox) { shape_.box = bbox; auto st = merged_own_style(); if (child_) { auto child_constraints = child_->size_constraints(); auto child_bbox = bbox; if (horizontal_scroll()) child_bbox[1].max -= width() * *st->scale; if (vertical_scroll()) child_bbox[0].max -= width() * *st->scale; if (width_first()) { child_constraints[1] = child_->height_constraints(child_bbox[0].length()); child_bbox[1].max = child_bbox[1].min + child_constraints[1].min; } else { child_constraints[0] = child_->width_constraints(child_bbox[1].length()); child_bbox[0].max = child_bbox[0].min + child_constraints[0].min; } { auto old_shift = shift_tgt_; clamp_shift(); if (shift_tgt_[0] != old_shift[0]) shift_[0] = shift_tgt_[0]; if (shift_tgt_[1] != old_shift[1]) shift_[1] = shift_tgt_[1]; } if (horizontal_scroll()) child_bbox[0] += shift_[0]; if (vertical_scroll()) child_bbox[1] += shift_[1]; child_->reshape(child_bbox); } } math::box scroller::size_constraints() const { math::box result; result[0] = {0.f, std::numeric_limits::infinity()}; result[1] = {0.f, std::numeric_limits::infinity()}; if (child_) { auto child_constraints = child_->size_constraints(); if (!horizontal_scroll()) result[0] = child_constraints[0]; if (!vertical_scroll()) result[1] = child_constraints[1]; } auto st = merged_own_style(); if (horizontal_scroll()) result[1] += width() * *st->scale; if (vertical_scroll()) result[0] += width() * *st->scale; return result; } math::interval scroller::width_constraints(float height) const { math::interval result = {0.f, std::numeric_limits::infinity()}; auto st = merged_own_style(); if (child_ && !horizontal_scroll()) result = child_->width_constraints(height - width() * *st->scale); if (vertical_scroll()) result += width() * *st->scale; return result; } math::interval scroller::height_constraints(float width) const { math::interval result = {0.f, std::numeric_limits::infinity()}; auto st = merged_own_style(); if (child_ && !horizontal_scroll()) result = child_->height_constraints(width - this->width() * *st->scale); if (horizontal_scroll()) result += this->width() * *st->scale; return result; } float scroller::position(direction dir) const { if (!child_) return 0.f; auto child_box = child_->shape().bbox(); auto st = merged_own_style(); auto child_area = shape_.box.dimensions(); if (horizontal_scroll()) child_area[1] -= width() * *st->scale; if (vertical_scroll()) child_area[0] -= width() * *st->scale; if (dir == direction::horizontal) return -shift_[0] / (child_box[0].length() - child_area[0]); else return -shift_[1] / (child_box[1].length() - child_area[1]); } void scroller::set_position(direction dir, float position, bool animate) { if (dir == direction::horizontal && !horizontal_scroll()) return; if (dir == direction::vertical && !vertical_scroll()) return; auto old_shift = shift_tgt_; int dim = (dir == direction::horizontal) ? 0 : 1; auto child_box = child_->shape().bbox(); auto st = merged_own_style(); auto child_area = shape_.box.dimensions(); if (horizontal_scroll()) child_area[1] -= width() * *st->scale; if (vertical_scroll()) child_area[0] -= width() * *st->scale; shift_tgt_[dim] = position * (child_area[dim] - child_box[dim].length()); clamp_shift(); if (!animate) shift_[dim] = shift_tgt_[dim]; on_scroll(shift_tgt_ - old_shift); } void scroller::update(float dt) { shift_ += (shift_tgt_ - shift_) * std::min(25.f * dt, 1.f); post_reshape(); } void scroller::draw(painter & p) const { auto st = merged_own_style(); float w = width() * *st->scale; auto box = shape_.box; if (horizontal_scroll()) box[1].max -= w; if (vertical_scroll()) box[0].max -= w; p.begin_stencil(); p.draw_rect(box, {0, 0, 0, 255}); p.commit_stencil(); } void scroller::post_draw(painter & p) const { p.end_stencil(); } void scroller::on_scroll(math::vector const &) {} float scroller::width() const { return 0; } math::box scroller::visible_range() const { if (!child_) return {{{0.f, 1.f}, {0.f, 1.f}}}; math::box result; auto c = child_->size_constraints(); result[0] = {- shift_[0] / c[0].min, (shape_.box[0].length() - shift_[0]) / c[0].min}; result[1] = {- shift_[1] / c[1].min, (shape_.box[1].length() - shift_[1]) / c[1].min}; result[0].max = std::min(result[0].max, 1.f); result[1].max = std::min(result[1].max, 1.f); return result; } math::box scroller::horizontal_box() const { auto box = shape_.box; float w = width() * *merged_own_style()->scale; box[1].min = box[1].max - w; if (vertical_scroll()) box[0].max -= w; return box; } math::box scroller::vertical_box() const { auto box = shape_.box; float w = width() * *merged_own_style()->scale; box[0].min = box[0].max - w; if (horizontal_scroll()) box[1].max -= w; return box; } void scroller::clamp_shift() { if (!child_) return; math::vector min = child_->shape().bbox().dimensions(); auto st = merged_own_style(); auto child_area = shape_.box.dimensions(); if (horizontal_scroll()) child_area[1] -= width() * *st->scale; if (vertical_scroll()) child_area[0] -= width() * *st->scale; min[0] = child_area[0] - min[0]; min[1] = child_area[1] - min[1]; // Can't use clamp since the order of min & max matters shift_tgt_[0] = std::max(shift_tgt_[0], min[0]); shift_tgt_[0] = std::min(shift_tgt_[0], 0.f); shift_tgt_[1] = std::max(shift_tgt_[1], min[1]); shift_tgt_[1] = std::min(shift_tgt_[1], 0.f); } }