diff --git a/libs/ui/include/psemek/ui/scroller.hpp b/libs/ui/include/psemek/ui/scroller.hpp index 185d055c..1aaf6df4 100644 --- a/libs/ui/include/psemek/ui/scroller.hpp +++ b/libs/ui/include/psemek/ui/scroller.hpp @@ -20,6 +20,7 @@ namespace psemek::ui virtual bool vertical_scroll() const { return vertical_; } virtual bool on_event(mouse_move const & e); + virtual bool on_event(mouse_click const & e); virtual bool on_event(mouse_wheel const & e); struct shape const & shape() const override { return shape_; } @@ -34,6 +35,24 @@ namespace psemek::ui ~scroller() override; + protected: + + virtual float width() const; + virtual geom::box visible_range() const; + + virtual geom::box horizontal_box() const; + virtual geom::box vertical_box() const; + + enum state_t + { + normal, + mouseover, + mousedown, + }; + + state_t vertical_state_ = state_t::normal; + state_t horizontal_state_ = state_t::normal; + private: box_shape shape_; std::optional> mouse_; @@ -46,6 +65,8 @@ namespace psemek::ui std::shared_ptr child_; element * children_[1]{nullptr}; + + void clamp_shift(); }; } diff --git a/libs/ui/source/scroller.cpp b/libs/ui/source/scroller.cpp index 28ac96ea..a76193be 100644 --- a/libs/ui/source/scroller.cpp +++ b/libs/ui/source/scroller.cpp @@ -32,6 +32,127 @@ namespace psemek::ui bool scroller::on_event(mouse_move const & e) { mouse_ = e.position; + + auto pos = geom::cast(e.position); + + if (horizontal_scroll()) + { + auto box = horizontal_box(); + bool over = geom::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; + + auto sc = child_->size_constraints(); + + float c = geom::unlerp(box[0], e.position[0]); + c = geom::clamp(c, {0.f, 1.f}); + + shift_tgt_[0] = shape_.box[0].length() / 2.f - c * sc[0].min; + clamp_shift(); + } + break; + } + } + + if (vertical_scroll()) + { + auto box = vertical_box(); + bool over = geom::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; + + auto sc = child_->size_constraints(); + + float c = geom::unlerp(box[1], e.position[1]); + c = geom::clamp(c, {0.f, 1.f}); + + shift_tgt_[1] = shape_.box[1].length() / 2.f - c * sc[1].min; + clamp_shift(); + } + break; + } + } + + return false; + } + + bool scroller::on_event(mouse_click const & e) + { + if (e.button != mouse_button::left) + return false; + + if (horizontal_scroll()) + { + switch (horizontal_state_) + { + case state_t::normal: + return false; + case state_t::mouseover: + if (e.down) + { + horizontal_state_ = state_t::mousedown; + return true; + } + return false; + case state_t::mousedown: + if (!e.down) + { + horizontal_state_ = state_t::mouseover; + return true; + } + return false; + } + } + + if (vertical_scroll()) + { + switch (vertical_state_) + { + case state_t::normal: + return false; + case state_t::mouseover: + if (e.down) + { + vertical_state_ = state_t::mousedown; + return true; + } + return false; + case state_t::mousedown: + if (!e.down) + { + vertical_state_ = state_t::mouseover; + return true; + } + return false; + } + } + return false; } @@ -39,18 +160,39 @@ namespace psemek::ui { if (mouse_ && geom::contains(shape_.box, geom::cast(*mouse_))) { - if (vertical_scroll()) + geom::vector delta = {0.f, 0.f}; + + if (vertical_scroll() && !horizontal_scroll()) { - shift_tgt_[1] += e.delta * 50.f; - post_reshape(); - return true; + delta[1] = e.delta; } - else if (horizontal_scroll()) + else if (!vertical_scroll() && horizontal_scroll()) { - shift_tgt_[0] += e.delta * 50.f; - post_reshape(); - return true; + delta[0] = e.delta; } + else + { + auto pos = geom::cast(*mouse_); + + if (horizontal_scroll()) + { + if (geom::contains(horizontal_box(), pos)) + { + delta[0] = e.delta; + } + } + + if (vertical_scroll()) + { + if (geom::contains(vertical_box(), pos)) + delta[1] = e.delta; + } + } + + shift_tgt_ += delta * 50.f; + clamp_shift(); + post_reshape(); + return true; } return false; } @@ -59,12 +201,19 @@ namespace psemek::ui { 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 (horizontal_scroll()) { child_bbox[0].min += shift_[0]; @@ -98,19 +247,35 @@ namespace psemek::ui result[1] = child_constraints[1]; } + auto st = merged_own_style(); + + if (horizontal_scroll()) + result[1].min = std::max(result[1].min, width() * *st->scale); + if (vertical_scroll()) + result[0].min = std::max(result[0].min, width() * *st->scale); + return result; } void scroller::update(float dt) { - shift_ += (shift_tgt_ - shift_) * std::min(10.f * dt, 1.f); + 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(shape_.box, {0, 0, 0, 255}); + p.draw_rect(box, {0, 0, 0, 255}); p.commit_stencil(); } @@ -124,4 +289,63 @@ namespace psemek::ui release_children(); } + float scroller::width() const + { + return 0; + } + + geom::box scroller::visible_range() const + { + if (!child_) + return {{{0.f, 1.f}, {0.f, 1.f}}}; + + geom::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; + } + + geom::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; + } + + geom::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; + + auto c = child_->size_constraints(); + + shift_tgt_[0] = std::max(shift_tgt_[0], shape_.box[0].length() - c[0].min); + shift_tgt_[0] = std::min(shift_tgt_[0], 0.f); + + shift_tgt_[1] = std::max(shift_tgt_[1], shape_.box[1].length() - c[1].min); + shift_tgt_[1] = std::min(shift_tgt_[1], 0.f); + } + }