diff options
46 files changed, 1996 insertions, 271 deletions
diff --git a/CruUI-Generate/cru_ui.cpp b/CruUI-Generate/cru_ui.cpp index 497af786..afd588d6 100644 --- a/CruUI-Generate/cru_ui.cpp +++ b/CruUI-Generate/cru_ui.cpp @@ -123,16 +123,6 @@ namespace cru { return instance_; } - namespace - { - void LoadSystemCursor(HINSTANCE h_instance) - { - ui::cursors::arrow = std::make_shared<ui::Cursor>(::LoadCursorW(nullptr, IDC_ARROW), false); - ui::cursors::hand = std::make_shared<ui::Cursor>(::LoadCursorW(nullptr, IDC_HAND), false); - ui::cursors::i_beam = std::make_shared<ui::Cursor>(::LoadCursorW(nullptr, IDC_IBEAM), false); - } - } - Application::Application(HINSTANCE h_instance) : h_instance_(h_instance) { @@ -141,9 +131,12 @@ namespace cru { instance_ = this; + if (!::IsWindows8OrGreater()) + throw std::runtime_error("Must run on Windows 8 or later."); + god_window_ = std::make_unique<GodWindow>(this); - LoadSystemCursor(h_instance); + ui::cursors::LoadSystemCursors(); } Application::~Application() @@ -267,6 +260,7 @@ namespace cru //-------begin of file: src\main.cpp //-------------------------------------------------------- + using cru::String; using cru::StringView; using cru::Application; @@ -284,6 +278,7 @@ using cru::ui::controls::Button; using cru::ui::controls::TextBox; using cru::ui::controls::ListItem; using cru::ui::controls::FrameLayout; +using cru::ui::controls::ScrollControl; int APIENTRY wWinMain( HINSTANCE hInstance, @@ -291,6 +286,10 @@ int APIENTRY wWinMain( LPWSTR lpCmdLine, int nCmdShow) { +#ifdef CRU_DEBUG + _CrtSetDbgFlag ( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF ); +#endif + Application application(hInstance); const auto window = Window::CreateOverlapped(); @@ -440,9 +439,16 @@ int APIENTRY wWinMain( } { - const auto text_block = CreateWithLayout<TextBlock>(LayoutSideParams::Stretch(), LayoutSideParams::Stretch(), L"This is a very very very very very long sentence!!!"); + const auto scroll_view = CreateWithLayout<ScrollControl>(LayoutSideParams::Stretch(), LayoutSideParams::Stretch()); + + scroll_view->SetVerticalScrollBarVisibility(ScrollControl::ScrollBarVisibility::Always); + + const auto text_block = TextBlock::Create( + L"Love myself I do. Not everything, but I love the good as well as the bad. I love my crazy lifestyle, and I love my hard discipline. I love my freedom of speech and the way my eyes get dark when I'm tired. I love that I have learned to trust people with my heart, even if it will get broken. I am proud of everything that I am and will become."); text_block->SetSelectable(true); - layout->AddChild(text_block); + + scroll_view->AddChild(text_block); + layout->AddChild(scroll_view); } layout->AddChild(CreateWithLayout<TextBlock>(LayoutSideParams::Content(Alignment::Start), LayoutSideParams::Content(), L"This is a little short sentence!!!")); @@ -473,7 +479,6 @@ int APIENTRY wWinMain( window.AddChild(linear_layout); */ - window->Show(); return application.Run(); @@ -992,26 +997,67 @@ namespace cru::ui bool Control::IsPointInside(const Point & point) { - if (border_geometry_ != nullptr) + const auto border_geometry = geometry_info_.border_geometry; + if (border_geometry != nullptr) { if (IsBordered()) { BOOL contains; - border_geometry_->FillContainsPoint(Convert(point), D2D1::Matrix3x2F::Identity(), &contains); + border_geometry->FillContainsPoint(Convert(point), D2D1::Matrix3x2F::Identity(), &contains); if (!contains) - border_geometry_->StrokeContainsPoint(Convert(point), GetBorderProperty().GetStrokeWidth(), nullptr, D2D1::Matrix3x2F::Identity(), &contains); + border_geometry->StrokeContainsPoint(Convert(point), GetBorderProperty().GetStrokeWidth(), nullptr, D2D1::Matrix3x2F::Identity(), &contains); return contains != 0; } else { BOOL contains; - border_geometry_->FillContainsPoint(Convert(point), D2D1::Matrix3x2F::Identity(), &contains); + border_geometry->FillContainsPoint(Convert(point), D2D1::Matrix3x2F::Identity(), &contains); return contains != 0; } } return false; } + Control* Control::HitTest(const Point& point) + { + const auto point_inside = IsPointInside(point); + + if (IsClipContent()) + { + if (!point_inside) + return nullptr; + if (geometry_info_.content_geometry != nullptr) + { + BOOL contains; + ThrowIfFailed(geometry_info_.content_geometry->FillContainsPoint(Convert(point), D2D1::Matrix3x2F::Identity(), &contains)); + if (contains == 0) + return this; + } + } + + const auto& children = GetChildren(); + + for (auto i = children.crbegin(); i != children.crend(); ++i) + { + const auto&& lefttop = (*i)->GetPositionRelative(); + const auto&& coerced_point = Point(point.x - lefttop.x, point.y - lefttop.y); + const auto child_hit_test_result = (*i)->HitTest(coerced_point); + if (child_hit_test_result != nullptr) + return child_hit_test_result; + } + + return point_inside ? this : nullptr; + } + + void Control::SetClipContent(const bool clip) + { + if (clip_content_ == clip) + return; + + clip_content_ = clip; + InvalidateDraw(); + } + void Control::Draw(ID2D1DeviceContext* device_context) { D2D1::Matrix3x2F old_transform; @@ -1020,11 +1066,20 @@ namespace cru::ui const auto position = GetPositionRelative(); device_context->SetTransform(old_transform * D2D1::Matrix3x2F::Translation(position.x, position.y)); + OnDrawDecoration(device_context); + + const auto set_layer = geometry_info_.content_geometry != nullptr && IsClipContent(); + if (set_layer) + device_context->PushLayer(D2D1::LayerParameters(D2D1::InfiniteRect(), geometry_info_.content_geometry.Get()), nullptr); + OnDrawCore(device_context); for (auto child : GetChildren()) child->Draw(device_context); + if (set_layer) + device_context->PopLayer(); + device_context->SetTransform(old_transform); } @@ -1067,6 +1122,7 @@ namespace cru::ui { SetPositionRelative(rect.GetLeftTop()); SetSize(rect.GetSize()); + AfterLayoutSelf(); OnLayoutCore(Rect(Point::Zero(), rect.GetSize())); } @@ -1136,7 +1192,7 @@ namespace cru::ui void Control::UpdateBorder() { - RegenerateBorderGeometry(); + RegenerateGeometryInfo(); InvalidateLayout(); InvalidateDraw(); } @@ -1168,7 +1224,6 @@ namespace cru::ui child->TraverseDescendants([window](Control* control) { control->OnAttachToWindow(window); }); - window->RefreshControlList(); InvalidateLayout(); } } @@ -1180,7 +1235,6 @@ namespace cru::ui child->TraverseDescendants([window](Control* control) { control->OnDetachToWindow(window); }); - window->RefreshControlList(); InvalidateLayout(); } } @@ -1195,9 +1249,9 @@ namespace cru::ui window_ = nullptr; } - void Control::OnDrawCore(ID2D1DeviceContext* device_context) + void Control::OnDrawDecoration(ID2D1DeviceContext* device_context) { - #ifdef CRU_DEBUG_LAYOUT +#ifdef CRU_DEBUG_LAYOUT if (GetWindow()->IsDebugLayout()) { if (padding_geometry_ != nullptr) @@ -1208,17 +1262,21 @@ namespace cru::ui } #endif - if (is_bordered_ && border_geometry_ != nullptr) + if (is_bordered_ && geometry_info_.border_geometry != nullptr) device_context->DrawGeometry( - border_geometry_.Get(), + geometry_info_.border_geometry.Get(), GetBorderProperty().GetBrush().Get(), GetBorderProperty().GetStrokeWidth(), GetBorderProperty().GetStrokeStyle().Get() ); + } + void Control::OnDrawCore(ID2D1DeviceContext* device_context) + { + const auto ground_geometry = geometry_info_.padding_content_geometry; //draw background. - if (in_border_geometry_ != nullptr && background_brush_ != nullptr) - device_context->FillGeometry(in_border_geometry_.Get(), background_brush_.Get()); + if (ground_geometry != nullptr && background_brush_ != nullptr) + device_context->FillGeometry(ground_geometry.Get(), background_brush_.Get()); const auto padding_rect = GetRect(RectRange::Padding); graph::WithTransform(device_context, D2D1::Matrix3x2F::Translation(padding_rect.left, padding_rect.top), [this](ID2D1DeviceContext* device_context) @@ -1240,8 +1298,8 @@ namespace cru::ui //draw foreground. - if (in_border_geometry_ != nullptr && foreground_brush_ != nullptr) - device_context->FillGeometry(in_border_geometry_.Get(), foreground_brush_.Get()); + if (ground_geometry != nullptr && foreground_brush_ != nullptr) + device_context->FillGeometry(ground_geometry.Get(), foreground_brush_.Get()); graph::WithTransform(device_context, D2D1::Matrix3x2F::Translation(padding_rect.left, padding_rect.top), [this](ID2D1DeviceContext* device_context) { @@ -1303,7 +1361,7 @@ namespace cru::ui void Control::OnSizeChangedCore(SizeChangedEventArgs & args) { - RegenerateBorderGeometry(); + RegenerateGeometryInfo(); #ifdef CRU_DEBUG_LAYOUT margin_geometry_ = CalculateSquareRingGeometry(GetRect(RectRange::Margin), GetRect(RectRange::FullBorder)); padding_geometry_ = CalculateSquareRingGeometry(GetRect(RectRange::Padding), GetRect(RectRange::Content)); @@ -1324,7 +1382,7 @@ namespace cru::ui size_changed_event.Raise(args); } - void Control::RegenerateBorderGeometry() + void Control::RegenerateGeometryInfo() { if (IsBordered()) { @@ -1337,10 +1395,10 @@ namespace cru::ui ThrowIfFailed( graph::GraphManager::GetInstance()->GetD2D1Factory()->CreateRoundedRectangleGeometry(bound_rounded_rect, &geometry) ); - border_geometry_ = std::move(geometry); + geometry_info_.border_geometry = std::move(geometry); - const auto in_border_rect = GetRect(RectRange::Padding); - const auto in_border_rounded_rect = D2D1::RoundedRect(Convert(in_border_rect), + const auto padding_rect = GetRect(RectRange::Padding); + const auto in_border_rounded_rect = D2D1::RoundedRect(Convert(padding_rect), GetBorderProperty().GetRadiusX() - GetBorderProperty().GetStrokeWidth() / 2.0f, GetBorderProperty().GetRadiusY() - GetBorderProperty().GetStrokeWidth() / 2.0f); @@ -1348,7 +1406,24 @@ namespace cru::ui ThrowIfFailed( graph::GraphManager::GetInstance()->GetD2D1Factory()->CreateRoundedRectangleGeometry(in_border_rounded_rect, &geometry2) ); - in_border_geometry_ = std::move(geometry2); + geometry_info_.padding_content_geometry = geometry2; + + + Microsoft::WRL::ComPtr<ID2D1RectangleGeometry> geometry3; + ThrowIfFailed( + graph::GraphManager::GetInstance()->GetD2D1Factory()->CreateRectangleGeometry(Convert(GetRect(RectRange::Content)), &geometry3) + ); + Microsoft::WRL::ComPtr<ID2D1PathGeometry> geometry4; + ThrowIfFailed( + graph::GraphManager::GetInstance()->GetD2D1Factory()->CreatePathGeometry(&geometry4) + ); + Microsoft::WRL::ComPtr<ID2D1GeometrySink> sink; + geometry4->Open(&sink); + ThrowIfFailed( + geometry3->CombineWithGeometry(geometry2.Get(), D2D1_COMBINE_MODE_INTERSECT, D2D1::Matrix3x2F::Identity(), sink.Get()) + ); + sink->Close(); + geometry_info_.content_geometry = std::move(geometry4); } else { @@ -1357,8 +1432,14 @@ namespace cru::ui ThrowIfFailed( graph::GraphManager::GetInstance()->GetD2D1Factory()->CreateRectangleGeometry(Convert(bound_rect), &geometry) ); - border_geometry_ = geometry; - in_border_geometry_ = std::move(geometry); + geometry_info_.border_geometry = geometry; + geometry_info_.padding_content_geometry = std::move(geometry); + + Microsoft::WRL::ComPtr<ID2D1RectangleGeometry> geometry2; + ThrowIfFailed( + graph::GraphManager::GetInstance()->GetD2D1Factory()->CreateRectangleGeometry(Convert(GetRect(RectRange::Content)), &geometry2) + ); + geometry_info_.content_geometry = std::move(geometry2); } } @@ -1433,6 +1514,16 @@ namespace cru::ui } + void Control::OnMouseWheel(events::MouseWheelEventArgs& args) + { + + } + + void Control::OnMouseWheelCore(events::MouseWheelEventArgs& args) + { + + } + void Control::RaiseMouseEnterEvent(MouseEventArgs& args) { OnMouseEnterCore(args); @@ -1475,6 +1566,13 @@ namespace cru::ui mouse_click_event.Raise(args); } + void Control::RaiseMouseWheelEvent(MouseWheelEventArgs& args) + { + OnMouseWheelCore(args); + OnMouseWheel(args); + mouse_wheel_event.Raise(args); + } + void Control::OnMouseClickBegin(MouseButton button) { @@ -1635,7 +1733,7 @@ namespace cru::ui auto parent = GetParent(); while (parent != nullptr) { - auto lp = parent->GetLayoutParams(); + const auto lp = parent->GetLayoutParams(); if (!stretch_width_determined) { @@ -1770,6 +1868,11 @@ namespace cru::ui } } + void Control::AfterLayoutSelf() + { + + } + void Control::CheckAndNotifyPositionChanged() { if (this->old_position_ != this->position_) @@ -1867,6 +1970,13 @@ namespace cru::ui Cursor::Ptr arrow{}; Cursor::Ptr hand{}; Cursor::Ptr i_beam{}; + + void LoadSystemCursors() + { + arrow = std::make_shared<Cursor>(::LoadCursorW(nullptr, IDC_ARROW), false); + hand = std::make_shared<Cursor>(::LoadCursorW(nullptr, IDC_HAND), false); + i_beam = std::make_shared<Cursor>(::LoadCursorW(nullptr, IDC_IBEAM), false); + } } } //-------------------------------------------------------- @@ -2060,7 +2170,11 @@ namespace cru::ui list_item_hover_border_brush {CreateSolidBrush(graph_manager, D2D1::ColorF(D2D1::ColorF::SkyBlue))}, list_item_hover_fill_brush {CreateSolidBrush(graph_manager, D2D1::ColorF(D2D1::ColorF::SkyBlue, 0.3f))}, list_item_select_border_brush {CreateSolidBrush(graph_manager, D2D1::ColorF(D2D1::ColorF::MediumBlue))}, - list_item_select_fill_brush {CreateSolidBrush(graph_manager, D2D1::ColorF(D2D1::ColorF::SkyBlue, 0.3f))} + list_item_select_fill_brush {CreateSolidBrush(graph_manager, D2D1::ColorF(D2D1::ColorF::SkyBlue, 0.3f))}, + + scroll_bar_background_brush {CreateSolidBrush(graph_manager, D2D1::ColorF(D2D1::ColorF::Gainsboro, 0.3f))}, + scroll_bar_border_brush {CreateSolidBrush(graph_manager, D2D1::ColorF(D2D1::ColorF::DimGray))}, + scroll_bar_brush {CreateSolidBrush(graph_manager, D2D1::ColorF(D2D1::ColorF::DimGray))} #ifdef CRU_DEBUG_LAYOUT , @@ -2221,7 +2335,8 @@ namespace cru::ui return new Window(tag_popup_constructor{}, parent, caption); } - Window::Window(tag_overlapped_constructor) : Control(WindowConstructorTag{}, this), control_list_({ this }) { + Window::Window(tag_overlapped_constructor) : Control(WindowConstructorTag{}, this) + { const auto window_manager = WindowManager::GetInstance(); hwnd_ = CreateWindowEx(0, @@ -2237,7 +2352,7 @@ namespace cru::ui AfterCreateHwnd(window_manager); } - Window::Window(tag_popup_constructor, Window* parent, const bool caption) : Control(WindowConstructorTag{}, this), control_list_({ this }) + Window::Window(tag_popup_constructor, Window* parent, const bool caption) : Control(WindowConstructorTag{}, this) { if (parent != nullptr && !parent->IsWindowValid()) throw std::runtime_error("Parent window is not valid."); @@ -2500,6 +2615,14 @@ namespace cru::ui result = 0; return true; } + case WM_MOUSEWHEEL: + POINT point; + point.x = GET_X_LPARAM(l_param); + point.y = GET_Y_LPARAM(l_param); + ScreenToClient(hwnd, &point); + OnMouseWheelInternal(GET_WHEEL_DELTA_WPARAM(w_param), point); + result = 0; + return true; case WM_KEYDOWN: OnKeyDownInternal(static_cast<int>(w_param)); result = 0; @@ -2595,24 +2718,6 @@ namespace cru::ui is_layout_invalid_ = false; } - void Window::RefreshControlList() { - control_list_.clear(); - TraverseDescendants([this](Control* control) { - this->control_list_.push_back(control); - }); - } - - Control * Window::HitTest(const Point & point) - { - for (auto i = control_list_.crbegin(); i != control_list_.crend(); ++i) { - auto control = *i; - if (control->IsPointInside(control->WindowToControl(point))) { - return control; - } - } - return nullptr; - } - bool Window::RequestFocusFor(Control * control) { if (control == nullptr) @@ -2828,6 +2933,20 @@ namespace cru::ui DispatchEvent(control, &Control::RaiseMouseUpEvent, nullptr, dip_point, button); } + void Window::OnMouseWheelInternal(short delta, POINT point) + { + const auto dip_point = PiToDip(point); + + Control* control; + + if (mouse_capture_control_) + control = mouse_capture_control_; + else + control = HitTest(dip_point); + + DispatchEvent(control, &Control::RaiseMouseWheelEvent, nullptr, dip_point, static_cast<float>(delta)); + } + void Window::OnKeyDownInternal(int virtual_code) { DispatchEvent(focus_control_, &Control::RaiseKeyDownEvent, nullptr, virtual_code); @@ -3113,6 +3232,7 @@ namespace cru::ui::controls #include <algorithm> + namespace cru::ui::controls { LinearLayout::LinearLayout(const Orientation orientation) @@ -3121,16 +3241,6 @@ namespace cru::ui::controls } - inline float AtLeast0(const float value) - { - return value < 0 ? 0 : value; - } - - inline Size AtLeast0(const Size& size) - { - return Size(AtLeast0(size.width), AtLeast0(size.height)); - } - StringView LinearLayout::GetControlType() const { return control_type; @@ -3395,6 +3505,404 @@ namespace cru::ui::controls //-------end of file: src\ui\controls\popup_menu.cpp //-------------------------------------------------------- //-------------------------------------------------------- +//-------begin of file: src\ui\controls\scroll_control.cpp +//-------------------------------------------------------- + +#include <limits> + + +namespace cru::ui::controls +{ + constexpr auto scroll_bar_width = 15.0f; + + ScrollControl::ScrollControl(const bool container) : Control(container) + { + SetClipContent(true); + } + + ScrollControl::~ScrollControl() + { + + } + + StringView ScrollControl::GetControlType() const + { + return control_type; + } + + void ScrollControl::SetHorizontalScrollEnabled(const bool enable) + { + horizontal_scroll_enabled_ = enable; + InvalidateLayout(); + InvalidateDraw(); + } + + void ScrollControl::SetVerticalScrollEnabled(const bool enable) + { + vertical_scroll_enabled_ = enable; + InvalidateLayout(); + InvalidateDraw(); + } + + void ScrollControl::SetHorizontalScrollBarVisibility(const ScrollBarVisibility visibility) + { + if (visibility != horizontal_scroll_bar_visibility_) + { + horizontal_scroll_bar_visibility_ = visibility; + switch (visibility) + { + case ScrollBarVisibility::Always: + is_horizontal_scroll_bar_visible_ = true; + break; + case ScrollBarVisibility::None: + is_horizontal_scroll_bar_visible_ = false; + break; + case ScrollBarVisibility::Auto: + UpdateScrollBarVisibility(); + } + InvalidateDraw(); + } + } + + void ScrollControl::SetVerticalScrollBarVisibility(const ScrollBarVisibility visibility) + { + if (visibility != vertical_scroll_bar_visibility_) + { + vertical_scroll_bar_visibility_ = visibility; + switch (visibility) + { + case ScrollBarVisibility::Always: + is_vertical_scroll_bar_visible_ = true; + break; + case ScrollBarVisibility::None: + is_vertical_scroll_bar_visible_ = false; + break; + case ScrollBarVisibility::Auto: + UpdateScrollBarVisibility(); + } + InvalidateDraw(); + } + + } + + void ScrollControl::SetScrollOffset(std::optional<float> x, std::optional<float> y) + { + CoerceAndSetOffsets(x.value_or(GetScrollOffsetX()), y.value_or(GetScrollOffsetY())); + } + + void ScrollControl::SetViewWidth(const float length) + { + view_width_ = length; + } + + void ScrollControl::SetViewHeight(const float length) + { + view_height_ = length; + } + + Size ScrollControl::OnMeasureContent(const Size& available_size) + { + const auto layout_params = GetLayoutParams(); + + auto available_size_for_children = available_size; + if (IsHorizontalScrollEnabled()) + { + if (layout_params->width.mode == MeasureMode::Content) + debug::DebugMessage(L"ScrollControl: Width measure mode is Content and horizontal scroll is enabled. So Stretch is used instead."); + + for (auto child : GetChildren()) + { + const auto child_layout_params = child->GetLayoutParams(); + if (child_layout_params->width.mode == MeasureMode::Stretch) + throw std::runtime_error(Format("ScrollControl: Horizontal scroll is enabled but a child {} 's width measure mode is Stretch which may cause infinite length.", ToUtf8String(child->GetControlType()))); + } + + available_size_for_children.width = std::numeric_limits<float>::max(); + } + + if (IsVerticalScrollEnabled()) + { + if (layout_params->height.mode == MeasureMode::Content) + debug::DebugMessage(L"ScrollControl: Height measure mode is Content and vertical scroll is enabled. So Stretch is used instead."); + + for (auto child : GetChildren()) + { + const auto child_layout_params = child->GetLayoutParams(); + if (child_layout_params->height.mode == MeasureMode::Stretch) + throw std::runtime_error(Format("ScrollControl: Vertical scroll is enabled but a child {} 's height measure mode is Stretch which may cause infinite length.", ToUtf8String(child->GetControlType()))); + } + + available_size_for_children.height = std::numeric_limits<float>::max(); + } + + auto max_child_size = Size::Zero(); + for (auto control: GetChildren()) + { + control->Measure(available_size_for_children); + const auto&& size = control->GetDesiredSize(); + if (max_child_size.width < size.width) + max_child_size.width = size.width; + if (max_child_size.height < size.height) + max_child_size.height = size.height; + } + + // coerce size fro stretch. + for (auto control: GetChildren()) + { + auto size = control->GetDesiredSize(); + const auto child_layout_params = control->GetLayoutParams(); + if (child_layout_params->width.mode == MeasureMode::Stretch) + size.width = max_child_size.width; + if (child_layout_params->height.mode == MeasureMode::Stretch) + size.height = max_child_size.height; + control->SetDesiredSize(size); + } + + auto result = max_child_size; + if (IsHorizontalScrollEnabled()) + { + SetViewWidth(max_child_size.width); + result.width = available_size.width; + } + if (IsVerticalScrollEnabled()) + { + SetViewHeight(max_child_size.height); + result.height = available_size.height; + } + + return result; + } + + void ScrollControl::OnLayoutContent(const Rect& rect) + { + auto layout_rect = rect; + + if (IsHorizontalScrollEnabled()) + layout_rect.width = GetViewWidth(); + if (IsVerticalScrollEnabled()) + layout_rect.height = GetViewHeight(); + + for (auto control: GetChildren()) + { + const auto size = control->GetDesiredSize(); + // Ignore alignment, always center aligned. + auto&& calculate_anchor = [](const float anchor, const float layout_length, const float control_length, const float offset) -> float + { + return anchor + (layout_length - control_length) / 2 - offset; + }; + + control->Layout(Rect(Point( + calculate_anchor(rect.left, layout_rect.width, size.width, offset_x_), + calculate_anchor(rect.top, layout_rect.height, size.height, offset_y_) + ), size)); + } + } + + void ScrollControl::AfterLayoutSelf() + { + UpdateScrollBarBorderInfo(); + CoerceAndSetOffsets(offset_x_, offset_y_, false); + UpdateScrollBarVisibility(); + } + + void ScrollControl::OnDrawForeground(ID2D1DeviceContext* device_context) + { + Control::OnDrawForeground(device_context); + + const auto predefined = UiManager::GetInstance()->GetPredefineResources(); + + if (is_horizontal_scroll_bar_visible_) + { + device_context->FillRectangle( + Convert(horizontal_bar_info_.border), + predefined->scroll_bar_background_brush.Get() + ); + + device_context->FillRectangle( + Convert(horizontal_bar_info_.bar), + predefined->scroll_bar_brush.Get() + ); + + device_context->DrawLine( + Convert(horizontal_bar_info_.border.GetLeftTop()), + Convert(horizontal_bar_info_.border.GetRightTop()), + predefined->scroll_bar_border_brush.Get() + ); + } + + if (is_vertical_scroll_bar_visible_) + { + device_context->FillRectangle( + Convert(vertical_bar_info_.border), + predefined->scroll_bar_background_brush.Get() + ); + + device_context->FillRectangle( + Convert(vertical_bar_info_.bar), + predefined->scroll_bar_brush.Get() + ); + + device_context->DrawLine( + Convert(vertical_bar_info_.border.GetLeftTop()), + Convert(vertical_bar_info_.border.GetLeftBottom()), + predefined->scroll_bar_border_brush.Get() + ); + } + } + + void ScrollControl::OnMouseDownCore(events::MouseButtonEventArgs& args) + { + Control::OnMouseDownCore(args); + + if (args.GetMouseButton() == MouseButton::Left) + { + const auto point = args.GetPoint(this); + if (is_vertical_scroll_bar_visible_ && vertical_bar_info_.bar.IsPointInside(point)) + { + GetWindow()->CaptureMouseFor(this); + is_pressing_scroll_bar_ = Orientation::Vertical; + pressing_delta_ = point.y - vertical_bar_info_.bar.top; + return; + } + + if (is_horizontal_scroll_bar_visible_ && horizontal_bar_info_.bar.IsPointInside(point)) + { + GetWindow()->CaptureMouseFor(this); + pressing_delta_ = point.x - horizontal_bar_info_.bar.left; + is_pressing_scroll_bar_ = Orientation::Horizontal; + return; + } + } + } + + void ScrollControl::OnMouseMoveCore(events::MouseEventArgs& args) + { + Control::OnMouseMoveCore(args); + + const auto mouse_point = args.GetPoint(this); + + if (is_pressing_scroll_bar_ == Orientation::Horizontal) + { + const auto new_head_position = mouse_point.x - pressing_delta_; + const auto new_offset = new_head_position / horizontal_bar_info_.border.width * view_width_; + SetScrollOffset(new_offset, std::nullopt); + return; + } + + if (is_pressing_scroll_bar_ == Orientation::Vertical) + { + const auto new_head_position = mouse_point.y - pressing_delta_; + const auto new_offset = new_head_position / vertical_bar_info_.border.height * view_height_; + SetScrollOffset(std::nullopt, new_offset); + return; + } + } + + void ScrollControl::OnMouseUpCore(events::MouseButtonEventArgs& args) + { + Control::OnMouseUpCore(args); + + if (args.GetMouseButton() == MouseButton::Left && is_pressing_scroll_bar_.has_value()) + { + GetWindow()->ReleaseCurrentMouseCapture(); + is_pressing_scroll_bar_ = std::nullopt; + } + } + + void ScrollControl::OnMouseWheelCore(events::MouseWheelEventArgs& args) + { + Control::OnMouseWheelCore(args); + + constexpr const auto view_delta = 30.0f; + + if (args.GetDelta() == 0.0f) + return; + + const auto content_rect = GetRect(RectRange::Content); + if (IsVerticalScrollEnabled() && GetScrollOffsetY() != (args.GetDelta() > 0.0f ? 0.0f : AtLeast0(GetViewHeight() - content_rect.height))) + { + SetScrollOffset(std::nullopt, GetScrollOffsetY() - args.GetDelta() / WHEEL_DELTA * view_delta); + return; + } + + if (IsHorizontalScrollEnabled() && GetScrollOffsetX() != (args.GetDelta() > 0.0f ? 0.0f : AtLeast0(GetViewWidth() - content_rect.width))) + { + SetScrollOffset(GetScrollOffsetX() - args.GetDelta() / WHEEL_DELTA * view_delta, std::nullopt); + return; + } + } + + void ScrollControl::CoerceAndSetOffsets(const float offset_x, const float offset_y, const bool update_children) + { + const auto old_offset_x = offset_x_; + const auto old_offset_y = offset_y_; + + const auto content_rect = GetRect(RectRange::Content); + offset_x_ = Coerce(offset_x, 0.0f, AtLeast0(view_width_ - content_rect.width)); + offset_y_ = Coerce(offset_y, 0.0f, AtLeast0(view_height_ - content_rect.height)); + UpdateScrollBarBarInfo(); + + if (update_children) + { + for (auto child : GetChildren()) + { + const auto old_position = child->GetPositionRelative(); + child->SetPositionRelative(Point( + old_position.x + old_offset_x - offset_x_, + old_position.y + old_offset_y - offset_y_ + )); + } + } + InvalidateDraw(); + } + + void ScrollControl::UpdateScrollBarVisibility() + { + const auto content_rect = GetRect(RectRange::Content); + if (GetHorizontalScrollBarVisibility() == ScrollBarVisibility::Auto) + is_horizontal_scroll_bar_visible_ = view_width_ > content_rect.width; + if (GetVerticalScrollBarVisibility() == ScrollBarVisibility::Auto) + is_vertical_scroll_bar_visible_ = view_height_ > content_rect.height; + } + + void ScrollControl::UpdateScrollBarBorderInfo() + { + const auto content_rect = GetRect(RectRange::Content); + horizontal_bar_info_.border = Rect(content_rect.left, content_rect.GetBottom() - scroll_bar_width, content_rect.width, scroll_bar_width); + vertical_bar_info_.border = Rect(content_rect.GetRight() - scroll_bar_width , content_rect.top, scroll_bar_width, content_rect.height); + } + + void ScrollControl::UpdateScrollBarBarInfo() + { + const auto content_rect = GetRect(RectRange::Content); + { + const auto& border = horizontal_bar_info_.border; + if (view_width_ <= content_rect.width) + horizontal_bar_info_.bar = border; + else + { + const auto bar_length = border.width * content_rect.width / view_width_; + const auto offset = border.width * offset_x_ / view_width_; + horizontal_bar_info_.bar = Rect(border.left + offset, border.top, bar_length, border.height); + } + } + { + const auto& border = vertical_bar_info_.border; + if (view_height_ <= content_rect.height) + vertical_bar_info_.bar = border; + else + { + const auto bar_length = border.height * content_rect.height / view_height_; + const auto offset = border.height * offset_y_ / view_height_; + vertical_bar_info_.bar = Rect(border.left, border.top + offset, border.width, bar_length); + } + } + } +} +//-------------------------------------------------------- +//-------end of file: src\ui\controls\scroll_control.cpp +//-------------------------------------------------------- +//-------------------------------------------------------- //-------begin of file: src\ui\controls\text_block.cpp //-------------------------------------------------------- @@ -3640,6 +4148,8 @@ namespace cru::ui::controls brush_ = init_brush; selection_brush_ = UiManager::GetInstance()->GetPredefineResources()->text_control_selection_brush; + + SetClipContent(true); } diff --git a/CruUI-Generate/cru_ui.hpp b/CruUI-Generate/cru_ui.hpp index 93360e3a..6dff57cd 100644 --- a/CruUI-Generate/cru_ui.hpp +++ b/CruUI-Generate/cru_ui.hpp @@ -3,19 +3,9 @@ //-------begin of file: src\any_map.hpp //-------------------------------------------------------- -#include <any> -#include <unordered_map> -#include <functional> -#include <optional> -#include <typeinfo> - -//-------------------------------------------------------- -//-------begin of file: src\base.hpp -//-------------------------------------------------------- - // ReSharper disable once CppUnusedIncludeDirective //-------------------------------------------------------- -//-------begin of file: src\global_macros.hpp +//-------begin of file: src\pre.hpp //-------------------------------------------------------- #ifdef _DEBUG @@ -25,18 +15,35 @@ #ifdef CRU_DEBUG #define CRU_DEBUG_LAYOUT #endif + + +#ifdef CRU_DEBUG +// ReSharper disable once IdentifierTypo +// ReSharper disable once CppInconsistentNaming +#define _CRTDBG_MAP_ALLOC +#include <cstdlib> +#include <crtdbg.h> +#endif +//-------------------------------------------------------- +//-------end of file: src\pre.hpp +//-------------------------------------------------------- + +#include <any> +#include <unordered_map> +#include <functional> +#include <optional> +#include <typeinfo> + //-------------------------------------------------------- -//-------end of file: src\global_macros.hpp +//-------begin of file: src\base.hpp //-------------------------------------------------------- +// ReSharper disable once CppUnusedIncludeDirective #include <string> #include <stdexcept> #include <string_view> #include <chrono> -#include <optional> -// ReSharper disable once CppUnusedIncludeDirective -#include <type_traits> namespace cru { @@ -86,33 +93,6 @@ namespace cru if (!condition) throw std::invalid_argument(error_message.data()); } - - template <typename T, typename = std::enable_if_t<std::is_arithmetic_v<T>>> - float Coerce(const T n, const std::optional<T> min, const std::optional<T> max) - { - if (min.has_value() && n < min.value()) - return min.value(); - if (max.has_value() && n > max.value()) - return max.value(); - return n; - } - - template <typename T, typename = std::enable_if_t<std::is_arithmetic_v<T>>> - float Coerce(const T n, const std::nullopt_t, const std::optional<T> max) - { - if (max.has_value() && n > max.value()) - return max.value(); - return n; - } - - template <typename T, typename = std::enable_if_t<std::is_arithmetic_v<T>>> - float Coerce(const T n, const std::optional<T> min, const std::nullopt_t) - { - if (min.has_value() && n < min.value()) - return min.value(); - return n; - } - } //-------------------------------------------------------- //-------end of file: src\base.hpp @@ -121,6 +101,8 @@ namespace cru //-------begin of file: src\format.hpp //-------------------------------------------------------- +// ReSharper disable once CppUnusedIncludeDirective + namespace cru { @@ -325,10 +307,13 @@ namespace cru //-------begin of file: src\application.hpp //-------------------------------------------------------- +// ReSharper disable once CppUnusedIncludeDirective + //-------------------------------------------------------- //-------begin of file: src\system_headers.hpp //-------------------------------------------------------- +// ReSharper disable once CppUnusedIncludeDirective //include system headers @@ -348,6 +333,8 @@ namespace cru #include <dxgi1_2.h> #include <wrl/client.h> + +#include <VersionHelpers.h> //-------------------------------------------------------- //-------end of file: src\system_headers.hpp //-------------------------------------------------------- @@ -469,6 +456,8 @@ namespace cru //-------begin of file: src\exception.hpp //-------------------------------------------------------- +// ReSharper disable once CppUnusedIncludeDirective + #include <optional> @@ -529,6 +518,7 @@ namespace cru { //-------begin of file: src\timer.hpp //-------------------------------------------------------- +// ReSharper disable once CppUnusedIncludeDirective #include <map> #include <chrono> @@ -594,14 +584,17 @@ namespace cru //-------begin of file: src\ui\window.hpp //-------------------------------------------------------- +// ReSharper disable once CppUnusedIncludeDirective + #include <map> -#include <list> #include <memory> //-------------------------------------------------------- //-------begin of file: src\ui\control.hpp //-------------------------------------------------------- +// ReSharper disable once CppUnusedIncludeDirective + #include <unordered_map> #include <any> #include <utility> @@ -610,6 +603,8 @@ namespace cru //-------begin of file: src\ui\ui_base.hpp //-------------------------------------------------------- +// ReSharper disable once CppUnusedIncludeDirective + #include <optional> @@ -760,6 +755,16 @@ namespace cru::ui return Point(left + width, top + height); } + constexpr Point GetLeftBottom() const + { + return Point(left, top + height); + } + + constexpr Point GetRightTop() const + { + return Point(left + width, top); + } + constexpr Size GetSize() const { return Size(width, height); @@ -832,6 +837,8 @@ namespace cru::ui //-------begin of file: src\ui\layout_base.hpp //-------------------------------------------------------- +// ReSharper disable once CppUnusedIncludeDirective + #include <unordered_set> @@ -977,12 +984,16 @@ namespace cru::ui //-------begin of file: src\ui\events\ui_event.hpp //-------------------------------------------------------- +// ReSharper disable once CppUnusedIncludeDirective + #include <optional> //-------------------------------------------------------- //-------begin of file: src\cru_event.hpp //-------------------------------------------------------- +// ReSharper disable once CppUnusedIncludeDirective + #include <type_traits> #include <functional> #include <unordered_map> @@ -1147,6 +1158,30 @@ namespace cru::ui::events }; + class MouseWheelEventArgs : public MouseEventArgs + { + public: + MouseWheelEventArgs(Object* sender, Object* original_sender, const Point& point, const float delta) + : MouseEventArgs(sender, original_sender, point), delta_(delta) + { + + } + MouseWheelEventArgs(const MouseWheelEventArgs& other) = default; + MouseWheelEventArgs(MouseWheelEventArgs&& other) = default; + MouseWheelEventArgs& operator=(const MouseWheelEventArgs& other) = default; + MouseWheelEventArgs& operator=(MouseWheelEventArgs&& other) = default; + ~MouseWheelEventArgs() override = default; + + float GetDelta() const + { + return delta_; + } + + private: + float delta_; + }; + + class DrawEventArgs : public UiEventArgs { public: @@ -1366,6 +1401,7 @@ namespace cru::ui::events using UiEvent = Event<UiEventArgs>; using MouseEvent = Event<MouseEventArgs>; using MouseButtonEvent = Event<MouseButtonEventArgs>; + using MouseWheelEvent = Event<MouseWheelEventArgs>; using DrawEvent = Event<DrawEventArgs>; using PositionChangedEvent = Event<PositionChangedEventArgs>; using SizeChangedEvent = Event<SizeChangedEventArgs>; @@ -1382,6 +1418,8 @@ namespace cru::ui::events //-------begin of file: src\ui\border_property.hpp //-------------------------------------------------------- +// ReSharper disable once CppUnusedIncludeDirective + @@ -1469,6 +1507,8 @@ namespace cru::ui //-------begin of file: src\ui\cursor.hpp //-------------------------------------------------------- +// ReSharper disable once CppUnusedIncludeDirective + #include <memory> @@ -1500,7 +1540,9 @@ namespace cru::ui { extern Cursor::Ptr arrow; extern Cursor::Ptr hand; - extern Cursor::Ptr i_beam; + extern Cursor::Ptr i_beam; + + void LoadSystemCursors(); } } //-------------------------------------------------------- @@ -1520,12 +1562,22 @@ namespace cru::ui Point lefttop_position_absolute; }; + class Control : public Object { friend class Window; friend class LayoutManager; protected: + struct GeometryInfo + { + Microsoft::WRL::ComPtr<ID2D1Geometry> border_geometry = nullptr; + Microsoft::WRL::ComPtr<ID2D1Geometry> padding_content_geometry = nullptr; + Microsoft::WRL::ComPtr<ID2D1Geometry> content_geometry = nullptr; + }; + + + protected: struct WindowConstructorTag {}; //Used for constructor for class Window. explicit Control(bool container = false); @@ -1615,9 +1667,18 @@ namespace cru::ui // fill and stroke with width of border. virtual bool IsPointInside(const Point& point); + // Get the top control among all descendants (including self) in local coordinate. + virtual Control* HitTest(const Point& point); //*************** region: graphic *************** + bool IsClipContent() const + { + return clip_content_; + } + + void SetClipContent(bool clip); + //Draw this control and its child controls. void Draw(ID2D1DeviceContext* device_context); @@ -1733,6 +1794,8 @@ namespace cru::ui //Raised when a mouse button is pressed in the control and released in the control with mouse not leaving it between two operations. events::MouseButtonEvent mouse_click_event; + events::MouseWheelEvent mouse_wheel_event; + events::KeyEvent key_down_event; events::KeyEvent key_up_event; events::CharEvent char_event; @@ -1758,12 +1821,11 @@ namespace cru::ui //Invoked when the control is detached to a window. Overrides should invoke base. virtual void OnDetachToWindow(Window* window); + //*************** region: graphic events *************** private: + void OnDrawDecoration(ID2D1DeviceContext* device_context); void OnDrawCore(ID2D1DeviceContext* device_context); - protected: - - //*************** region: graphic events *************** virtual void OnDrawContent(ID2D1DeviceContext* device_context); virtual void OnDrawForeground(ID2D1DeviceContext* device_context); virtual void OnDrawBackground(ID2D1DeviceContext* device_context); @@ -1785,7 +1847,12 @@ namespace cru::ui void RaisePositionChangedEvent(events::PositionChangedEventArgs& args); void RaiseSizeChangedEvent(events::SizeChangedEventArgs& args); - void RegenerateBorderGeometry(); + void RegenerateGeometryInfo(); + + const GeometryInfo& GetGeometryInfo() const + { + return geometry_info_; + } //*************** region: mouse event *************** virtual void OnMouseEnter(events::MouseEventArgs& args); @@ -1802,6 +1869,9 @@ namespace cru::ui virtual void OnMouseUpCore(events::MouseButtonEventArgs& args); virtual void OnMouseClickCore(events::MouseButtonEventArgs& args); + virtual void OnMouseWheel(events::MouseWheelEventArgs& args); + virtual void OnMouseWheelCore(events::MouseWheelEventArgs& args); + void RaiseMouseEnterEvent(events::MouseEventArgs& args); void RaiseMouseLeaveEvent(events::MouseEventArgs& args); void RaiseMouseMoveEvent(events::MouseEventArgs& args); @@ -1809,6 +1879,8 @@ namespace cru::ui void RaiseMouseUpEvent(events::MouseButtonEventArgs& args); void RaiseMouseClickEvent(events::MouseButtonEventArgs& args); + void RaiseMouseWheelEvent(events::MouseWheelEventArgs& args); + virtual void OnMouseClickBegin(MouseButton button); virtual void OnMouseClickEnd(MouseButton button); @@ -1842,6 +1914,9 @@ namespace cru::ui virtual Size OnMeasureContent(const Size& available_size); virtual void OnLayoutContent(const Rect& rect); + // Called by Layout after set position and size. + virtual void AfterLayoutSelf(); + private: // Only for layout manager to use. // Check if the old position is updated to current position. @@ -1858,10 +1933,8 @@ namespace cru::ui private: bool is_container_; - protected: - Window * window_ = nullptr; // protected for Window class to write it as itself in constructor. + Window * window_ = nullptr; - private: Control * parent_ = nullptr; std::vector<Control*> children_{}; @@ -1891,8 +1964,9 @@ namespace cru::ui bool is_bordered_ = false; BorderProperty border_property_; - Microsoft::WRL::ComPtr<ID2D1Geometry> border_geometry_ = nullptr; - Microsoft::WRL::ComPtr<ID2D1Geometry> in_border_geometry_ = nullptr; //used for foreground and background brush. + GeometryInfo geometry_info_{}; + + bool clip_content_ = false; Microsoft::WRL::ComPtr<ID2D1Brush> foreground_brush_ = nullptr; Microsoft::WRL::ComPtr<ID2D1Brush> background_brush_ = nullptr; @@ -2155,15 +2229,6 @@ namespace cru::ui void SetSizeFitContent(const Size& max_size = Size(1000, 1000)); - //*************** region: functions *************** - - //Refresh control list. - //It should be invoked every time a control is added or removed from the tree. - void RefreshControlList(); - - //Get the most top control at "point". - Control* HitTest(const Point& point); - //*************** region: focus *************** @@ -2224,7 +2289,8 @@ namespace cru::ui void OnMouseLeaveInternal(); void OnMouseDownInternal(MouseButton button, POINT point); void OnMouseUpInternal(MouseButton button, POINT point); - + + void OnMouseWheelInternal(short delta, POINT point); void OnKeyDownInternal(int virtual_code); void OnKeyUpInternal(int virtual_code); void OnCharInternal(wchar_t c); @@ -2267,8 +2333,6 @@ namespace cru::ui Window* parent_window_ = nullptr; std::shared_ptr<graph::WindowRenderTarget> render_target_{}; - std::list<Control*> control_list_{}; - Control* mouse_hover_control_ = nullptr; bool window_focus_ = false; @@ -2290,6 +2354,8 @@ namespace cru::ui //-------begin of file: src\cru_debug.hpp //-------------------------------------------------------- +// ReSharper disable once CppUnusedIncludeDirective + #include <functional> @@ -2337,6 +2403,8 @@ namespace cru::debug //-------begin of file: src\ui\controls\linear_layout.hpp //-------------------------------------------------------- +// ReSharper disable once CppUnusedIncludeDirective + namespace cru::ui::controls { @@ -2388,10 +2456,14 @@ namespace cru::ui::controls //-------begin of file: src\ui\controls\text_block.hpp //-------------------------------------------------------- +// ReSharper disable once CppUnusedIncludeDirective + //-------------------------------------------------------- //-------begin of file: src\ui\controls\text_control.hpp //-------------------------------------------------------- +// ReSharper disable once CppUnusedIncludeDirective + namespace cru::ui::controls { @@ -2529,6 +2601,8 @@ namespace cru::ui::controls //-------begin of file: src\ui\controls\toggle_button.hpp //-------------------------------------------------------- +// ReSharper disable once CppUnusedIncludeDirective + namespace cru::ui::controls { @@ -2598,6 +2672,8 @@ namespace cru::ui::controls //-------begin of file: src\ui\controls\button.hpp //-------------------------------------------------------- +// ReSharper disable once CppUnusedIncludeDirective + #include <initializer_list> @@ -2644,6 +2720,8 @@ namespace cru::ui::controls //-------begin of file: src\ui\controls\text_box.hpp //-------------------------------------------------------- +// ReSharper disable once CppUnusedIncludeDirective + namespace cru::ui::controls { @@ -2699,6 +2777,8 @@ namespace cru::ui::controls //-------begin of file: src\ui\controls\list_item.hpp //-------------------------------------------------------- +// ReSharper disable once CppUnusedIncludeDirective + #include <map> #include <initializer_list> @@ -2770,6 +2850,8 @@ namespace cru::ui::controls //-------begin of file: src\ui\controls\popup_menu.hpp //-------------------------------------------------------- +// ReSharper disable once CppUnusedIncludeDirective + #include <vector> #include <utility> #include <functional> @@ -2793,6 +2875,8 @@ namespace cru::ui::controls //-------begin of file: src\ui\controls\frame_layout.hpp //-------------------------------------------------------- +// ReSharper disable once CppUnusedIncludeDirective + #include <initializer_list> @@ -2827,9 +2911,175 @@ namespace cru::ui::controls //-------end of file: src\ui\controls\frame_layout.hpp //-------------------------------------------------------- //-------------------------------------------------------- +//-------begin of file: src\ui\controls\scroll_control.hpp +//-------------------------------------------------------- + +// ReSharper disable once CppUnusedIncludeDirective + +#include <optional> +#include <initializer_list> + + +namespace cru::ui::controls +{ + // Done: OnMeasureContent + // Done: OnLayoutContent + // Done: HitTest(no need) + // Done: Draw(no need) + // Done: API + // Done: ScrollBar + // Done: MouseEvent + class ScrollControl : public Control + { + private: + struct ScrollBarInfo + { + Rect border = Rect(); + Rect bar = Rect(); + }; + + enum class Orientation + { + Horizontal, + Vertical + }; + + public: + enum class ScrollBarVisibility + { + None, + Auto, + Always + }; + + static ScrollControl* Create(const std::initializer_list<Control*>& children = std::initializer_list<Control*>{}) + { + const auto control = new ScrollControl(true); + for (auto child : children) + control->AddChild(child); + return control; + } + + static constexpr auto control_type = L"ScrollControl"; + + protected: + explicit ScrollControl(bool container); + public: + ScrollControl(const ScrollControl& other) = delete; + ScrollControl(ScrollControl&& other) = delete; + ScrollControl& operator=(const ScrollControl& other) = delete; + ScrollControl& operator=(ScrollControl&& other) = delete; + ~ScrollControl() override; + + StringView GetControlType() const override final; + + bool IsHorizontalScrollEnabled() const + { + return horizontal_scroll_enabled_; + } + + void SetHorizontalScrollEnabled(bool enable); + + bool IsVerticalScrollEnabled() const + { + return vertical_scroll_enabled_; + } + + void SetVerticalScrollEnabled(bool enable); + + + ScrollBarVisibility GetHorizontalScrollBarVisibility() const + { + return horizontal_scroll_bar_visibility_; + } + + void SetHorizontalScrollBarVisibility(ScrollBarVisibility visibility); + + ScrollBarVisibility GetVerticalScrollBarVisibility() const + { + return vertical_scroll_bar_visibility_; + } + + void SetVerticalScrollBarVisibility(ScrollBarVisibility visibility); + + float GetViewWidth() const + { + return view_width_; + } + + float GetViewHeight() const + { + return view_height_; + } + + float GetScrollOffsetX() const + { + return offset_x_; + } + + float GetScrollOffsetY() const + { + return offset_y_; + } + + // nullopt for not set. value is auto-coerced. + void SetScrollOffset(std::optional<float> x, std::optional<float> y); + + protected: + void SetViewWidth(float length); + void SetViewHeight(float length); + + Size OnMeasureContent(const Size& available_size) override final; + void OnLayoutContent(const Rect& rect) override final; + + void AfterLayoutSelf() override; + + void OnDrawForeground(ID2D1DeviceContext* device_context) override; + + void OnMouseDownCore(events::MouseButtonEventArgs& args) override final; + void OnMouseMoveCore(events::MouseEventArgs& args) override final; + void OnMouseUpCore(events::MouseButtonEventArgs& args) override final; + + void OnMouseWheelCore(events::MouseWheelEventArgs& args) override; + + private: + void CoerceAndSetOffsets(float offset_x, float offset_y, bool update_children = true); + void UpdateScrollBarVisibility(); + void UpdateScrollBarBorderInfo(); + void UpdateScrollBarBarInfo(); + + private: + bool horizontal_scroll_enabled_ = true; + bool vertical_scroll_enabled_ = true; + + ScrollBarVisibility horizontal_scroll_bar_visibility_ = ScrollBarVisibility::Auto; + ScrollBarVisibility vertical_scroll_bar_visibility_ = ScrollBarVisibility::Auto; + + bool is_horizontal_scroll_bar_visible_ = false; + bool is_vertical_scroll_bar_visible_ = false; + + float offset_x_ = 0.0f; + float offset_y_ = 0.0f; + + float view_width_ = 0.0f; + float view_height_ = 0.0f; + + ScrollBarInfo horizontal_bar_info_; + ScrollBarInfo vertical_bar_info_; + + std::optional<Orientation> is_pressing_scroll_bar_ = std::nullopt; + float pressing_delta_ = 0.0f; + }; +} +//-------------------------------------------------------- +//-------end of file: src\ui\controls\scroll_control.hpp +//-------------------------------------------------------- +//-------------------------------------------------------- //-------begin of file: src\graph\graph.hpp //-------------------------------------------------------- +// ReSharper disable once CppUnusedIncludeDirective + #include <memory> #include <functional> @@ -3010,6 +3260,8 @@ namespace cru::graph //-------begin of file: src\ui\ui_manager.hpp //-------------------------------------------------------- +// ReSharper disable once CppUnusedIncludeDirective + namespace cru::graph @@ -3068,6 +3320,10 @@ namespace cru::ui Microsoft::WRL::ComPtr<ID2D1Brush> list_item_select_border_brush; Microsoft::WRL::ComPtr<ID2D1Brush> list_item_select_fill_brush; + //region ScrollControl + Microsoft::WRL::ComPtr<ID2D1Brush> scroll_bar_background_brush; + Microsoft::WRL::ComPtr<ID2D1Brush> scroll_bar_border_brush; + Microsoft::WRL::ComPtr<ID2D1Brush> scroll_bar_brush; #ifdef CRU_DEBUG_LAYOUT //region debug @@ -3113,6 +3369,8 @@ namespace cru::ui //-------begin of file: src\ui\convert_util.hpp //-------------------------------------------------------- +// ReSharper disable once CppUnusedIncludeDirective + namespace cru::ui @@ -3131,9 +3389,84 @@ namespace cru::ui //-------end of file: src\ui\convert_util.hpp //-------------------------------------------------------- //-------------------------------------------------------- +//-------begin of file: src\math_util.hpp +//-------------------------------------------------------- + +// ReSharper disable once CppUnusedIncludeDirective + +// ReSharper disable once CppUnusedIncludeDirective +#include <type_traits> +#include <optional> + +namespace cru +{ + template <typename T, typename = std::enable_if_t<std::is_arithmetic_v<T>>> + float Coerce(const T n, const std::optional<T> min, const std::optional<T> max) + { + if (min.has_value() && n < min.value()) + return min.value(); + if (max.has_value() && n > max.value()) + return max.value(); + return n; + } + + template <typename T, typename = std::enable_if_t<std::is_arithmetic_v<T>>> + float Coerce(const T n, const T min, const T max) + { + if (n < min) + return min; + if (n > max) + return max; + return n; + } + + template <typename T, typename = std::enable_if_t<std::is_arithmetic_v<T>>> + float Coerce(const T n, const std::nullopt_t, const std::optional<T> max) + { + if (max.has_value() && n > max.value()) + return max.value(); + return n; + } + + template <typename T, typename = std::enable_if_t<std::is_arithmetic_v<T>>> + float Coerce(const T n, const std::optional<T> min, const std::nullopt_t) + { + if (min.has_value() && n < min.value()) + return min.value(); + return n; + } + + template <typename T, typename = std::enable_if_t<std::is_arithmetic_v<T>>> + float Coerce(const T n, const std::nullopt_t, const T max) + { + if (n > max) + return max; + return n; + } + + template <typename T, typename = std::enable_if_t<std::is_arithmetic_v<T>>> + float Coerce(const T n, const T min, const std::nullopt_t) + { + if (n < min) + return min; + return n; + } + + template <typename T, typename = std::enable_if_t<std::is_arithmetic_v<T>>> + T AtLeast0(const T value) + { + return value < static_cast<T>(0) ? static_cast<T>(0) : value; + } +} +//-------------------------------------------------------- +//-------end of file: src\math_util.hpp +//-------------------------------------------------------- +//-------------------------------------------------------- //-------begin of file: src\ui\animations\animation.hpp //-------------------------------------------------------- +// ReSharper disable once CppUnusedIncludeDirective + #include <unordered_map> diff --git a/CruUI.vcxproj b/CruUI.vcxproj index 932726cf..30f39f28 100644 --- a/CruUI.vcxproj +++ b/CruUI.vcxproj @@ -133,14 +133,17 @@ <ClCompile Include="src\ui\controls\linear_layout.cpp" /> <ClCompile Include="src\ui\controls\list_item.cpp" /> <ClCompile Include="src\ui\controls\popup_menu.cpp" /> + <ClCompile Include="src\ui\controls\scroll_control.cpp" /> <ClCompile Include="src\ui\controls\text_block.cpp" /> <ClCompile Include="src\ui\controls\text_box.cpp" /> <ClInclude Include="src\any_map.hpp" /> <ClInclude Include="src\format.hpp" /> + <ClInclude Include="src\math_util.hpp" /> <ClInclude Include="src\ui\border_property.hpp" /> <ClInclude Include="src\ui\controls\frame_layout.hpp" /> <ClInclude Include="src\ui\controls\list_item.hpp" /> <ClInclude Include="src\ui\controls\popup_menu.hpp" /> + <ClInclude Include="src\ui\controls\scroll_control.hpp" /> <ClInclude Include="src\ui\controls\text_control.hpp" /> <ClCompile Include="src\ui\controls\toggle_button.cpp" /> <ClCompile Include="src\ui\cursor.cpp" /> @@ -156,7 +159,7 @@ <ClInclude Include="src\cru_event.hpp" /> <ClInclude Include="src\cru_debug.hpp" /> <ClInclude Include="src\exception.hpp" /> - <ClInclude Include="src\global_macros.hpp" /> + <ClInclude Include="src\pre.hpp" /> <ClInclude Include="src\graph\graph.hpp" /> <ClInclude Include="src\system_headers.hpp" /> <ClInclude Include="src\timer.hpp" /> diff --git a/CruUI.vcxproj.filters b/CruUI.vcxproj.filters index b8c89bb1..71025931 100644 --- a/CruUI.vcxproj.filters +++ b/CruUI.vcxproj.filters @@ -90,6 +90,9 @@ <ClCompile Include="src\ui\controls\frame_layout.cpp"> <Filter>Source Files</Filter> </ClCompile> + <ClCompile Include="src\ui\controls\scroll_control.cpp"> + <Filter>Source Files</Filter> + </ClCompile> </ItemGroup> <ItemGroup> <ClInclude Include="src\graph\graph.hpp"> @@ -155,9 +158,6 @@ <ClInclude Include="src\format.hpp"> <Filter>Header Files</Filter> </ClInclude> - <ClInclude Include="src\global_macros.hpp"> - <Filter>Header Files</Filter> - </ClInclude> <ClInclude Include="src\system_headers.hpp"> <Filter>Header Files</Filter> </ClInclude> @@ -182,6 +182,15 @@ <ClInclude Include="src\ui\controls\frame_layout.hpp"> <Filter>Header Files</Filter> </ClInclude> + <ClInclude Include="src\ui\controls\scroll_control.hpp"> + <Filter>Header Files</Filter> + </ClInclude> + <ClInclude Include="src\math_util.hpp"> + <Filter>Header Files</Filter> + </ClInclude> + <ClInclude Include="src\pre.hpp"> + <Filter>Header Files</Filter> + </ClInclude> </ItemGroup> <ItemGroup> <ClCompile Include="src\application.cpp"> diff --git a/src/any_map.hpp b/src/any_map.hpp index ea6044b1..dfc54f3f 100644 --- a/src/any_map.hpp +++ b/src/any_map.hpp @@ -1,5 +1,8 @@ #pragma once +// ReSharper disable once CppUnusedIncludeDirective +#include "pre.hpp" + #include <any> #include <unordered_map> #include <functional> diff --git a/src/application.cpp b/src/application.cpp index fa71c37e..c3669f72 100644 --- a/src/application.cpp +++ b/src/application.cpp @@ -86,16 +86,6 @@ namespace cru { return instance_; } - namespace - { - void LoadSystemCursor(HINSTANCE h_instance) - { - ui::cursors::arrow = std::make_shared<ui::Cursor>(::LoadCursorW(nullptr, IDC_ARROW), false); - ui::cursors::hand = std::make_shared<ui::Cursor>(::LoadCursorW(nullptr, IDC_HAND), false); - ui::cursors::i_beam = std::make_shared<ui::Cursor>(::LoadCursorW(nullptr, IDC_IBEAM), false); - } - } - Application::Application(HINSTANCE h_instance) : h_instance_(h_instance) { @@ -104,9 +94,12 @@ namespace cru { instance_ = this; + if (!::IsWindows8OrGreater()) + throw std::runtime_error("Must run on Windows 8 or later."); + god_window_ = std::make_unique<GodWindow>(this); - LoadSystemCursor(h_instance); + ui::cursors::LoadSystemCursors(); } Application::~Application() diff --git a/src/application.hpp b/src/application.hpp index b9427826..a8d59cc8 100644 --- a/src/application.hpp +++ b/src/application.hpp @@ -1,5 +1,8 @@ #pragma once +// ReSharper disable once CppUnusedIncludeDirective +#include "pre.hpp" + #include "system_headers.hpp" #include <memory> #include <optional> diff --git a/src/base.hpp b/src/base.hpp index 5d8cb9ce..64ce7f6e 100644 --- a/src/base.hpp +++ b/src/base.hpp @@ -1,16 +1,12 @@ #pragma once // ReSharper disable once CppUnusedIncludeDirective -#include "global_macros.hpp" - +#include "pre.hpp" #include <string> #include <stdexcept> #include <string_view> #include <chrono> -#include <optional> -// ReSharper disable once CppUnusedIncludeDirective -#include <type_traits> namespace cru { @@ -60,31 +56,4 @@ namespace cru if (!condition) throw std::invalid_argument(error_message.data()); } - - template <typename T, typename = std::enable_if_t<std::is_arithmetic_v<T>>> - float Coerce(const T n, const std::optional<T> min, const std::optional<T> max) - { - if (min.has_value() && n < min.value()) - return min.value(); - if (max.has_value() && n > max.value()) - return max.value(); - return n; - } - - template <typename T, typename = std::enable_if_t<std::is_arithmetic_v<T>>> - float Coerce(const T n, const std::nullopt_t, const std::optional<T> max) - { - if (max.has_value() && n > max.value()) - return max.value(); - return n; - } - - template <typename T, typename = std::enable_if_t<std::is_arithmetic_v<T>>> - float Coerce(const T n, const std::optional<T> min, const std::nullopt_t) - { - if (min.has_value() && n < min.value()) - return min.value(); - return n; - } - } diff --git a/src/cru_debug.hpp b/src/cru_debug.hpp index ed6fcaf6..17cc7b53 100644 --- a/src/cru_debug.hpp +++ b/src/cru_debug.hpp @@ -1,5 +1,8 @@ #pragma once +// ReSharper disable once CppUnusedIncludeDirective +#include "pre.hpp" + #include <functional> #include "base.hpp" diff --git a/src/cru_event.hpp b/src/cru_event.hpp index 76a36b22..69832a0e 100644 --- a/src/cru_event.hpp +++ b/src/cru_event.hpp @@ -1,5 +1,8 @@ #pragma once +// ReSharper disable once CppUnusedIncludeDirective +#include "pre.hpp" + #include <type_traits> #include <functional> #include <unordered_map> diff --git a/src/exception.hpp b/src/exception.hpp index 68558478..b8cef604 100644 --- a/src/exception.hpp +++ b/src/exception.hpp @@ -1,5 +1,8 @@ #pragma once +// ReSharper disable once CppUnusedIncludeDirective +#include "pre.hpp" + #include "system_headers.hpp" #include <optional> diff --git a/src/format.hpp b/src/format.hpp index 3f6253ff..efd25f89 100644 --- a/src/format.hpp +++ b/src/format.hpp @@ -1,5 +1,8 @@ #pragma once +// ReSharper disable once CppUnusedIncludeDirective +#include "pre.hpp" + #include "base.hpp" namespace cru diff --git a/src/global_macros.hpp b/src/global_macros.hpp deleted file mode 100644 index eda57187..00000000 --- a/src/global_macros.hpp +++ /dev/null @@ -1,9 +0,0 @@ -#pragma once - -#ifdef _DEBUG -#define CRU_DEBUG -#endif - -#ifdef CRU_DEBUG -#define CRU_DEBUG_LAYOUT -#endif diff --git a/src/graph/graph.hpp b/src/graph/graph.hpp index 7771b48f..440b0594 100644 --- a/src/graph/graph.hpp +++ b/src/graph/graph.hpp @@ -1,5 +1,8 @@ #pragma once +// ReSharper disable once CppUnusedIncludeDirective +#include "pre.hpp" + #include "system_headers.hpp" #include <memory> #include <functional> diff --git a/src/main.cpp b/src/main.cpp index 376c03b8..7b27d95e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,3 +1,5 @@ +#include "pre.hpp" + #include "application.hpp" #include "ui/window.hpp" #include "ui/controls/linear_layout.hpp" @@ -8,6 +10,7 @@ #include "ui/controls/list_item.hpp" #include "ui/controls/popup_menu.hpp" #include "ui/controls/frame_layout.hpp" +#include "ui/controls/scroll_control.hpp" #include "graph/graph.hpp" using cru::String; @@ -27,6 +30,7 @@ using cru::ui::controls::Button; using cru::ui::controls::TextBox; using cru::ui::controls::ListItem; using cru::ui::controls::FrameLayout; +using cru::ui::controls::ScrollControl; int APIENTRY wWinMain( HINSTANCE hInstance, @@ -34,6 +38,10 @@ int APIENTRY wWinMain( LPWSTR lpCmdLine, int nCmdShow) { +#ifdef CRU_DEBUG + _CrtSetDbgFlag ( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF ); +#endif + Application application(hInstance); const auto window = Window::CreateOverlapped(); @@ -183,9 +191,16 @@ int APIENTRY wWinMain( } { - const auto text_block = CreateWithLayout<TextBlock>(LayoutSideParams::Stretch(), LayoutSideParams::Stretch(), L"This is a very very very very very long sentence!!!"); + const auto scroll_view = CreateWithLayout<ScrollControl>(LayoutSideParams::Stretch(), LayoutSideParams::Stretch()); + + scroll_view->SetVerticalScrollBarVisibility(ScrollControl::ScrollBarVisibility::Always); + + const auto text_block = TextBlock::Create( + L"Love myself I do. Not everything, but I love the good as well as the bad. I love my crazy lifestyle, and I love my hard discipline. I love my freedom of speech and the way my eyes get dark when I'm tired. I love that I have learned to trust people with my heart, even if it will get broken. I am proud of everything that I am and will become."); text_block->SetSelectable(true); - layout->AddChild(text_block); + + scroll_view->AddChild(text_block); + layout->AddChild(scroll_view); } layout->AddChild(CreateWithLayout<TextBlock>(LayoutSideParams::Content(Alignment::Start), LayoutSideParams::Content(), L"This is a little short sentence!!!")); @@ -216,7 +231,6 @@ int APIENTRY wWinMain( window.AddChild(linear_layout); */ - window->Show(); return application.Run(); diff --git a/src/math_util.hpp b/src/math_util.hpp new file mode 100644 index 00000000..b9830d6b --- /dev/null +++ b/src/math_util.hpp @@ -0,0 +1,69 @@ +#pragma once + +// ReSharper disable once CppUnusedIncludeDirective +#include "pre.hpp" + +// ReSharper disable once CppUnusedIncludeDirective +#include <type_traits> +#include <optional> + +namespace cru +{ + template <typename T, typename = std::enable_if_t<std::is_arithmetic_v<T>>> + float Coerce(const T n, const std::optional<T> min, const std::optional<T> max) + { + if (min.has_value() && n < min.value()) + return min.value(); + if (max.has_value() && n > max.value()) + return max.value(); + return n; + } + + template <typename T, typename = std::enable_if_t<std::is_arithmetic_v<T>>> + float Coerce(const T n, const T min, const T max) + { + if (n < min) + return min; + if (n > max) + return max; + return n; + } + + template <typename T, typename = std::enable_if_t<std::is_arithmetic_v<T>>> + float Coerce(const T n, const std::nullopt_t, const std::optional<T> max) + { + if (max.has_value() && n > max.value()) + return max.value(); + return n; + } + + template <typename T, typename = std::enable_if_t<std::is_arithmetic_v<T>>> + float Coerce(const T n, const std::optional<T> min, const std::nullopt_t) + { + if (min.has_value() && n < min.value()) + return min.value(); + return n; + } + + template <typename T, typename = std::enable_if_t<std::is_arithmetic_v<T>>> + float Coerce(const T n, const std::nullopt_t, const T max) + { + if (n > max) + return max; + return n; + } + + template <typename T, typename = std::enable_if_t<std::is_arithmetic_v<T>>> + float Coerce(const T n, const T min, const std::nullopt_t) + { + if (n < min) + return min; + return n; + } + + template <typename T, typename = std::enable_if_t<std::is_arithmetic_v<T>>> + T AtLeast0(const T value) + { + return value < static_cast<T>(0) ? static_cast<T>(0) : value; + } +} diff --git a/src/pre.hpp b/src/pre.hpp new file mode 100644 index 00000000..03c51a94 --- /dev/null +++ b/src/pre.hpp @@ -0,0 +1,18 @@ +#pragma once + +#ifdef _DEBUG +#define CRU_DEBUG +#endif + +#ifdef CRU_DEBUG +#define CRU_DEBUG_LAYOUT +#endif + + +#ifdef CRU_DEBUG +// ReSharper disable once IdentifierTypo +// ReSharper disable once CppInconsistentNaming +#define _CRTDBG_MAP_ALLOC +#include <cstdlib> +#include <crtdbg.h> +#endif diff --git a/src/system_headers.hpp b/src/system_headers.hpp index 99c091e1..eabc7c25 100644 --- a/src/system_headers.hpp +++ b/src/system_headers.hpp @@ -1,5 +1,7 @@ #pragma once +// ReSharper disable once CppUnusedIncludeDirective +#include "pre.hpp" //include system headers @@ -19,3 +21,5 @@ #include <dxgi1_2.h> #include <wrl/client.h> + +#include <VersionHelpers.h> diff --git a/src/timer.hpp b/src/timer.hpp index 3488db45..5055a3d8 100644 --- a/src/timer.hpp +++ b/src/timer.hpp @@ -1,5 +1,7 @@ #pragma once +// ReSharper disable once CppUnusedIncludeDirective +#include "pre.hpp" #include "system_headers.hpp" #include <map> diff --git a/src/ui/animations/animation.hpp b/src/ui/animations/animation.hpp index f25e4699..2226f021 100644 --- a/src/ui/animations/animation.hpp +++ b/src/ui/animations/animation.hpp @@ -1,5 +1,8 @@ #pragma once +// ReSharper disable once CppUnusedIncludeDirective +#include "pre.hpp" + #include <unordered_map> #include "base.hpp" diff --git a/src/ui/border_property.hpp b/src/ui/border_property.hpp index 7766f5a3..4dee0e0f 100644 --- a/src/ui/border_property.hpp +++ b/src/ui/border_property.hpp @@ -1,5 +1,8 @@ #pragma once +// ReSharper disable once CppUnusedIncludeDirective +#include "pre.hpp" + #include "system_headers.hpp" #include "base.hpp" diff --git a/src/ui/control.cpp b/src/ui/control.cpp index 8b91b25a..2a81427a 100644 --- a/src/ui/control.cpp +++ b/src/ui/control.cpp @@ -8,6 +8,7 @@ #include "exception.hpp" #include "cru_debug.hpp" #include "convert_util.hpp" +#include "math_util.hpp" #ifdef CRU_DEBUG_LAYOUT #include "ui_manager.hpp" @@ -191,26 +192,67 @@ namespace cru::ui bool Control::IsPointInside(const Point & point) { - if (border_geometry_ != nullptr) + const auto border_geometry = geometry_info_.border_geometry; + if (border_geometry != nullptr) { if (IsBordered()) { BOOL contains; - border_geometry_->FillContainsPoint(Convert(point), D2D1::Matrix3x2F::Identity(), &contains); + border_geometry->FillContainsPoint(Convert(point), D2D1::Matrix3x2F::Identity(), &contains); if (!contains) - border_geometry_->StrokeContainsPoint(Convert(point), GetBorderProperty().GetStrokeWidth(), nullptr, D2D1::Matrix3x2F::Identity(), &contains); + border_geometry->StrokeContainsPoint(Convert(point), GetBorderProperty().GetStrokeWidth(), nullptr, D2D1::Matrix3x2F::Identity(), &contains); return contains != 0; } else { BOOL contains; - border_geometry_->FillContainsPoint(Convert(point), D2D1::Matrix3x2F::Identity(), &contains); + border_geometry->FillContainsPoint(Convert(point), D2D1::Matrix3x2F::Identity(), &contains); return contains != 0; } } return false; } + Control* Control::HitTest(const Point& point) + { + const auto point_inside = IsPointInside(point); + + if (IsClipContent()) + { + if (!point_inside) + return nullptr; + if (geometry_info_.content_geometry != nullptr) + { + BOOL contains; + ThrowIfFailed(geometry_info_.content_geometry->FillContainsPoint(Convert(point), D2D1::Matrix3x2F::Identity(), &contains)); + if (contains == 0) + return this; + } + } + + const auto& children = GetChildren(); + + for (auto i = children.crbegin(); i != children.crend(); ++i) + { + const auto&& lefttop = (*i)->GetPositionRelative(); + const auto&& coerced_point = Point(point.x - lefttop.x, point.y - lefttop.y); + const auto child_hit_test_result = (*i)->HitTest(coerced_point); + if (child_hit_test_result != nullptr) + return child_hit_test_result; + } + + return point_inside ? this : nullptr; + } + + void Control::SetClipContent(const bool clip) + { + if (clip_content_ == clip) + return; + + clip_content_ = clip; + InvalidateDraw(); + } + void Control::Draw(ID2D1DeviceContext* device_context) { D2D1::Matrix3x2F old_transform; @@ -219,11 +261,20 @@ namespace cru::ui const auto position = GetPositionRelative(); device_context->SetTransform(old_transform * D2D1::Matrix3x2F::Translation(position.x, position.y)); + OnDrawDecoration(device_context); + + const auto set_layer = geometry_info_.content_geometry != nullptr && IsClipContent(); + if (set_layer) + device_context->PushLayer(D2D1::LayerParameters(D2D1::InfiniteRect(), geometry_info_.content_geometry.Get()), nullptr); + OnDrawCore(device_context); for (auto child : GetChildren()) child->Draw(device_context); + if (set_layer) + device_context->PopLayer(); + device_context->SetTransform(old_transform); } @@ -266,6 +317,7 @@ namespace cru::ui { SetPositionRelative(rect.GetLeftTop()); SetSize(rect.GetSize()); + AfterLayoutSelf(); OnLayoutCore(Rect(Point::Zero(), rect.GetSize())); } @@ -335,7 +387,7 @@ namespace cru::ui void Control::UpdateBorder() { - RegenerateBorderGeometry(); + RegenerateGeometryInfo(); InvalidateLayout(); InvalidateDraw(); } @@ -367,7 +419,6 @@ namespace cru::ui child->TraverseDescendants([window](Control* control) { control->OnAttachToWindow(window); }); - window->RefreshControlList(); InvalidateLayout(); } } @@ -379,7 +430,6 @@ namespace cru::ui child->TraverseDescendants([window](Control* control) { control->OnDetachToWindow(window); }); - window->RefreshControlList(); InvalidateLayout(); } } @@ -394,9 +444,9 @@ namespace cru::ui window_ = nullptr; } - void Control::OnDrawCore(ID2D1DeviceContext* device_context) + void Control::OnDrawDecoration(ID2D1DeviceContext* device_context) { - #ifdef CRU_DEBUG_LAYOUT +#ifdef CRU_DEBUG_LAYOUT if (GetWindow()->IsDebugLayout()) { if (padding_geometry_ != nullptr) @@ -407,17 +457,21 @@ namespace cru::ui } #endif - if (is_bordered_ && border_geometry_ != nullptr) + if (is_bordered_ && geometry_info_.border_geometry != nullptr) device_context->DrawGeometry( - border_geometry_.Get(), + geometry_info_.border_geometry.Get(), GetBorderProperty().GetBrush().Get(), GetBorderProperty().GetStrokeWidth(), GetBorderProperty().GetStrokeStyle().Get() ); + } + void Control::OnDrawCore(ID2D1DeviceContext* device_context) + { + const auto ground_geometry = geometry_info_.padding_content_geometry; //draw background. - if (in_border_geometry_ != nullptr && background_brush_ != nullptr) - device_context->FillGeometry(in_border_geometry_.Get(), background_brush_.Get()); + if (ground_geometry != nullptr && background_brush_ != nullptr) + device_context->FillGeometry(ground_geometry.Get(), background_brush_.Get()); const auto padding_rect = GetRect(RectRange::Padding); graph::WithTransform(device_context, D2D1::Matrix3x2F::Translation(padding_rect.left, padding_rect.top), [this](ID2D1DeviceContext* device_context) @@ -439,8 +493,8 @@ namespace cru::ui //draw foreground. - if (in_border_geometry_ != nullptr && foreground_brush_ != nullptr) - device_context->FillGeometry(in_border_geometry_.Get(), foreground_brush_.Get()); + if (ground_geometry != nullptr && foreground_brush_ != nullptr) + device_context->FillGeometry(ground_geometry.Get(), foreground_brush_.Get()); graph::WithTransform(device_context, D2D1::Matrix3x2F::Translation(padding_rect.left, padding_rect.top), [this](ID2D1DeviceContext* device_context) { @@ -502,7 +556,7 @@ namespace cru::ui void Control::OnSizeChangedCore(SizeChangedEventArgs & args) { - RegenerateBorderGeometry(); + RegenerateGeometryInfo(); #ifdef CRU_DEBUG_LAYOUT margin_geometry_ = CalculateSquareRingGeometry(GetRect(RectRange::Margin), GetRect(RectRange::FullBorder)); padding_geometry_ = CalculateSquareRingGeometry(GetRect(RectRange::Padding), GetRect(RectRange::Content)); @@ -523,7 +577,7 @@ namespace cru::ui size_changed_event.Raise(args); } - void Control::RegenerateBorderGeometry() + void Control::RegenerateGeometryInfo() { if (IsBordered()) { @@ -536,10 +590,10 @@ namespace cru::ui ThrowIfFailed( graph::GraphManager::GetInstance()->GetD2D1Factory()->CreateRoundedRectangleGeometry(bound_rounded_rect, &geometry) ); - border_geometry_ = std::move(geometry); + geometry_info_.border_geometry = std::move(geometry); - const auto in_border_rect = GetRect(RectRange::Padding); - const auto in_border_rounded_rect = D2D1::RoundedRect(Convert(in_border_rect), + const auto padding_rect = GetRect(RectRange::Padding); + const auto in_border_rounded_rect = D2D1::RoundedRect(Convert(padding_rect), GetBorderProperty().GetRadiusX() - GetBorderProperty().GetStrokeWidth() / 2.0f, GetBorderProperty().GetRadiusY() - GetBorderProperty().GetStrokeWidth() / 2.0f); @@ -547,7 +601,24 @@ namespace cru::ui ThrowIfFailed( graph::GraphManager::GetInstance()->GetD2D1Factory()->CreateRoundedRectangleGeometry(in_border_rounded_rect, &geometry2) ); - in_border_geometry_ = std::move(geometry2); + geometry_info_.padding_content_geometry = geometry2; + + + Microsoft::WRL::ComPtr<ID2D1RectangleGeometry> geometry3; + ThrowIfFailed( + graph::GraphManager::GetInstance()->GetD2D1Factory()->CreateRectangleGeometry(Convert(GetRect(RectRange::Content)), &geometry3) + ); + Microsoft::WRL::ComPtr<ID2D1PathGeometry> geometry4; + ThrowIfFailed( + graph::GraphManager::GetInstance()->GetD2D1Factory()->CreatePathGeometry(&geometry4) + ); + Microsoft::WRL::ComPtr<ID2D1GeometrySink> sink; + geometry4->Open(&sink); + ThrowIfFailed( + geometry3->CombineWithGeometry(geometry2.Get(), D2D1_COMBINE_MODE_INTERSECT, D2D1::Matrix3x2F::Identity(), sink.Get()) + ); + sink->Close(); + geometry_info_.content_geometry = std::move(geometry4); } else { @@ -556,8 +627,14 @@ namespace cru::ui ThrowIfFailed( graph::GraphManager::GetInstance()->GetD2D1Factory()->CreateRectangleGeometry(Convert(bound_rect), &geometry) ); - border_geometry_ = geometry; - in_border_geometry_ = std::move(geometry); + geometry_info_.border_geometry = geometry; + geometry_info_.padding_content_geometry = std::move(geometry); + + Microsoft::WRL::ComPtr<ID2D1RectangleGeometry> geometry2; + ThrowIfFailed( + graph::GraphManager::GetInstance()->GetD2D1Factory()->CreateRectangleGeometry(Convert(GetRect(RectRange::Content)), &geometry2) + ); + geometry_info_.content_geometry = std::move(geometry2); } } @@ -632,6 +709,16 @@ namespace cru::ui } + void Control::OnMouseWheel(events::MouseWheelEventArgs& args) + { + + } + + void Control::OnMouseWheelCore(events::MouseWheelEventArgs& args) + { + + } + void Control::RaiseMouseEnterEvent(MouseEventArgs& args) { OnMouseEnterCore(args); @@ -674,6 +761,13 @@ namespace cru::ui mouse_click_event.Raise(args); } + void Control::RaiseMouseWheelEvent(MouseWheelEventArgs& args) + { + OnMouseWheelCore(args); + OnMouseWheel(args); + mouse_wheel_event.Raise(args); + } + void Control::OnMouseClickBegin(MouseButton button) { @@ -834,7 +928,7 @@ namespace cru::ui auto parent = GetParent(); while (parent != nullptr) { - auto lp = parent->GetLayoutParams(); + const auto lp = parent->GetLayoutParams(); if (!stretch_width_determined) { @@ -969,6 +1063,11 @@ namespace cru::ui } } + void Control::AfterLayoutSelf() + { + + } + void Control::CheckAndNotifyPositionChanged() { if (this->old_position_ != this->position_) diff --git a/src/ui/control.hpp b/src/ui/control.hpp index 2ca5fa9e..d6ad9f02 100644 --- a/src/ui/control.hpp +++ b/src/ui/control.hpp @@ -1,5 +1,8 @@ #pragma once +// ReSharper disable once CppUnusedIncludeDirective +#include "pre.hpp" + #include "system_headers.hpp" #include <unordered_map> #include <any> @@ -26,12 +29,22 @@ namespace cru::ui Point lefttop_position_absolute; }; + class Control : public Object { friend class Window; friend class LayoutManager; protected: + struct GeometryInfo + { + Microsoft::WRL::ComPtr<ID2D1Geometry> border_geometry = nullptr; + Microsoft::WRL::ComPtr<ID2D1Geometry> padding_content_geometry = nullptr; + Microsoft::WRL::ComPtr<ID2D1Geometry> content_geometry = nullptr; + }; + + + protected: struct WindowConstructorTag {}; //Used for constructor for class Window. explicit Control(bool container = false); @@ -121,9 +134,18 @@ namespace cru::ui // fill and stroke with width of border. virtual bool IsPointInside(const Point& point); + // Get the top control among all descendants (including self) in local coordinate. + virtual Control* HitTest(const Point& point); //*************** region: graphic *************** + bool IsClipContent() const + { + return clip_content_; + } + + void SetClipContent(bool clip); + //Draw this control and its child controls. void Draw(ID2D1DeviceContext* device_context); @@ -239,6 +261,8 @@ namespace cru::ui //Raised when a mouse button is pressed in the control and released in the control with mouse not leaving it between two operations. events::MouseButtonEvent mouse_click_event; + events::MouseWheelEvent mouse_wheel_event; + events::KeyEvent key_down_event; events::KeyEvent key_up_event; events::CharEvent char_event; @@ -264,12 +288,11 @@ namespace cru::ui //Invoked when the control is detached to a window. Overrides should invoke base. virtual void OnDetachToWindow(Window* window); + //*************** region: graphic events *************** private: + void OnDrawDecoration(ID2D1DeviceContext* device_context); void OnDrawCore(ID2D1DeviceContext* device_context); - protected: - - //*************** region: graphic events *************** virtual void OnDrawContent(ID2D1DeviceContext* device_context); virtual void OnDrawForeground(ID2D1DeviceContext* device_context); virtual void OnDrawBackground(ID2D1DeviceContext* device_context); @@ -291,7 +314,12 @@ namespace cru::ui void RaisePositionChangedEvent(events::PositionChangedEventArgs& args); void RaiseSizeChangedEvent(events::SizeChangedEventArgs& args); - void RegenerateBorderGeometry(); + void RegenerateGeometryInfo(); + + const GeometryInfo& GetGeometryInfo() const + { + return geometry_info_; + } //*************** region: mouse event *************** virtual void OnMouseEnter(events::MouseEventArgs& args); @@ -308,6 +336,9 @@ namespace cru::ui virtual void OnMouseUpCore(events::MouseButtonEventArgs& args); virtual void OnMouseClickCore(events::MouseButtonEventArgs& args); + virtual void OnMouseWheel(events::MouseWheelEventArgs& args); + virtual void OnMouseWheelCore(events::MouseWheelEventArgs& args); + void RaiseMouseEnterEvent(events::MouseEventArgs& args); void RaiseMouseLeaveEvent(events::MouseEventArgs& args); void RaiseMouseMoveEvent(events::MouseEventArgs& args); @@ -315,6 +346,8 @@ namespace cru::ui void RaiseMouseUpEvent(events::MouseButtonEventArgs& args); void RaiseMouseClickEvent(events::MouseButtonEventArgs& args); + void RaiseMouseWheelEvent(events::MouseWheelEventArgs& args); + virtual void OnMouseClickBegin(MouseButton button); virtual void OnMouseClickEnd(MouseButton button); @@ -348,6 +381,9 @@ namespace cru::ui virtual Size OnMeasureContent(const Size& available_size); virtual void OnLayoutContent(const Rect& rect); + // Called by Layout after set position and size. + virtual void AfterLayoutSelf(); + private: // Only for layout manager to use. // Check if the old position is updated to current position. @@ -364,10 +400,8 @@ namespace cru::ui private: bool is_container_; - protected: - Window * window_ = nullptr; // protected for Window class to write it as itself in constructor. + Window * window_ = nullptr; - private: Control * parent_ = nullptr; std::vector<Control*> children_{}; @@ -397,8 +431,9 @@ namespace cru::ui bool is_bordered_ = false; BorderProperty border_property_; - Microsoft::WRL::ComPtr<ID2D1Geometry> border_geometry_ = nullptr; - Microsoft::WRL::ComPtr<ID2D1Geometry> in_border_geometry_ = nullptr; //used for foreground and background brush. + GeometryInfo geometry_info_{}; + + bool clip_content_ = false; Microsoft::WRL::ComPtr<ID2D1Brush> foreground_brush_ = nullptr; Microsoft::WRL::ComPtr<ID2D1Brush> background_brush_ = nullptr; diff --git a/src/ui/controls/button.hpp b/src/ui/controls/button.hpp index 50640b11..c53f7ed9 100644 --- a/src/ui/controls/button.hpp +++ b/src/ui/controls/button.hpp @@ -1,5 +1,8 @@ #pragma once +// ReSharper disable once CppUnusedIncludeDirective +#include "pre.hpp" + #include <initializer_list> #include "ui/control.hpp" diff --git a/src/ui/controls/frame_layout.hpp b/src/ui/controls/frame_layout.hpp index ca022780..45971584 100644 --- a/src/ui/controls/frame_layout.hpp +++ b/src/ui/controls/frame_layout.hpp @@ -1,5 +1,8 @@ #pragma once +// ReSharper disable once CppUnusedIncludeDirective +#include "pre.hpp" + #include <initializer_list> #include "ui/control.hpp" diff --git a/src/ui/controls/linear_layout.cpp b/src/ui/controls/linear_layout.cpp index 3789b305..8fb91513 100644 --- a/src/ui/controls/linear_layout.cpp +++ b/src/ui/controls/linear_layout.cpp @@ -2,6 +2,8 @@ #include <algorithm> +#include "math_util.hpp" + namespace cru::ui::controls { LinearLayout::LinearLayout(const Orientation orientation) @@ -10,16 +12,6 @@ namespace cru::ui::controls } - inline float AtLeast0(const float value) - { - return value < 0 ? 0 : value; - } - - inline Size AtLeast0(const Size& size) - { - return Size(AtLeast0(size.width), AtLeast0(size.height)); - } - StringView LinearLayout::GetControlType() const { return control_type; diff --git a/src/ui/controls/linear_layout.hpp b/src/ui/controls/linear_layout.hpp index b7ca42ec..deb51bd1 100644 --- a/src/ui/controls/linear_layout.hpp +++ b/src/ui/controls/linear_layout.hpp @@ -1,5 +1,8 @@ #pragma once +// ReSharper disable once CppUnusedIncludeDirective +#include "pre.hpp" + #include "ui/control.hpp" namespace cru::ui::controls diff --git a/src/ui/controls/list_item.hpp b/src/ui/controls/list_item.hpp index 1de89b5f..a77d13e6 100644 --- a/src/ui/controls/list_item.hpp +++ b/src/ui/controls/list_item.hpp @@ -1,5 +1,8 @@ #pragma once +// ReSharper disable once CppUnusedIncludeDirective +#include "pre.hpp" + #include <map> #include <initializer_list> diff --git a/src/ui/controls/popup_menu.hpp b/src/ui/controls/popup_menu.hpp index d47e3eb6..a2916590 100644 --- a/src/ui/controls/popup_menu.hpp +++ b/src/ui/controls/popup_menu.hpp @@ -1,5 +1,8 @@ #pragma once +// ReSharper disable once CppUnusedIncludeDirective +#include "pre.hpp" + #include <vector> #include <utility> #include <functional> diff --git a/src/ui/controls/scroll_control.cpp b/src/ui/controls/scroll_control.cpp new file mode 100644 index 00000000..aa5403d4 --- /dev/null +++ b/src/ui/controls/scroll_control.cpp @@ -0,0 +1,400 @@ +#include "scroll_control.hpp" + +#include <limits> + +#include "cru_debug.hpp" +#include "format.hpp" +#include "ui/convert_util.hpp" +#include "exception.hpp" +#include "math_util.hpp" +#include "ui/ui_manager.hpp" +#include "ui/window.hpp" + +namespace cru::ui::controls +{ + constexpr auto scroll_bar_width = 15.0f; + + ScrollControl::ScrollControl(const bool container) : Control(container) + { + SetClipContent(true); + } + + ScrollControl::~ScrollControl() + { + + } + + StringView ScrollControl::GetControlType() const + { + return control_type; + } + + void ScrollControl::SetHorizontalScrollEnabled(const bool enable) + { + horizontal_scroll_enabled_ = enable; + InvalidateLayout(); + InvalidateDraw(); + } + + void ScrollControl::SetVerticalScrollEnabled(const bool enable) + { + vertical_scroll_enabled_ = enable; + InvalidateLayout(); + InvalidateDraw(); + } + + void ScrollControl::SetHorizontalScrollBarVisibility(const ScrollBarVisibility visibility) + { + if (visibility != horizontal_scroll_bar_visibility_) + { + horizontal_scroll_bar_visibility_ = visibility; + switch (visibility) + { + case ScrollBarVisibility::Always: + is_horizontal_scroll_bar_visible_ = true; + break; + case ScrollBarVisibility::None: + is_horizontal_scroll_bar_visible_ = false; + break; + case ScrollBarVisibility::Auto: + UpdateScrollBarVisibility(); + } + InvalidateDraw(); + } + } + + void ScrollControl::SetVerticalScrollBarVisibility(const ScrollBarVisibility visibility) + { + if (visibility != vertical_scroll_bar_visibility_) + { + vertical_scroll_bar_visibility_ = visibility; + switch (visibility) + { + case ScrollBarVisibility::Always: + is_vertical_scroll_bar_visible_ = true; + break; + case ScrollBarVisibility::None: + is_vertical_scroll_bar_visible_ = false; + break; + case ScrollBarVisibility::Auto: + UpdateScrollBarVisibility(); + } + InvalidateDraw(); + } + + } + + void ScrollControl::SetScrollOffset(std::optional<float> x, std::optional<float> y) + { + CoerceAndSetOffsets(x.value_or(GetScrollOffsetX()), y.value_or(GetScrollOffsetY())); + } + + void ScrollControl::SetViewWidth(const float length) + { + view_width_ = length; + } + + void ScrollControl::SetViewHeight(const float length) + { + view_height_ = length; + } + + Size ScrollControl::OnMeasureContent(const Size& available_size) + { + const auto layout_params = GetLayoutParams(); + + auto available_size_for_children = available_size; + if (IsHorizontalScrollEnabled()) + { + if (layout_params->width.mode == MeasureMode::Content) + debug::DebugMessage(L"ScrollControl: Width measure mode is Content and horizontal scroll is enabled. So Stretch is used instead."); + + for (auto child : GetChildren()) + { + const auto child_layout_params = child->GetLayoutParams(); + if (child_layout_params->width.mode == MeasureMode::Stretch) + throw std::runtime_error(Format("ScrollControl: Horizontal scroll is enabled but a child {} 's width measure mode is Stretch which may cause infinite length.", ToUtf8String(child->GetControlType()))); + } + + available_size_for_children.width = std::numeric_limits<float>::max(); + } + + if (IsVerticalScrollEnabled()) + { + if (layout_params->height.mode == MeasureMode::Content) + debug::DebugMessage(L"ScrollControl: Height measure mode is Content and vertical scroll is enabled. So Stretch is used instead."); + + for (auto child : GetChildren()) + { + const auto child_layout_params = child->GetLayoutParams(); + if (child_layout_params->height.mode == MeasureMode::Stretch) + throw std::runtime_error(Format("ScrollControl: Vertical scroll is enabled but a child {} 's height measure mode is Stretch which may cause infinite length.", ToUtf8String(child->GetControlType()))); + } + + available_size_for_children.height = std::numeric_limits<float>::max(); + } + + auto max_child_size = Size::Zero(); + for (auto control: GetChildren()) + { + control->Measure(available_size_for_children); + const auto&& size = control->GetDesiredSize(); + if (max_child_size.width < size.width) + max_child_size.width = size.width; + if (max_child_size.height < size.height) + max_child_size.height = size.height; + } + + // coerce size fro stretch. + for (auto control: GetChildren()) + { + auto size = control->GetDesiredSize(); + const auto child_layout_params = control->GetLayoutParams(); + if (child_layout_params->width.mode == MeasureMode::Stretch) + size.width = max_child_size.width; + if (child_layout_params->height.mode == MeasureMode::Stretch) + size.height = max_child_size.height; + control->SetDesiredSize(size); + } + + auto result = max_child_size; + if (IsHorizontalScrollEnabled()) + { + SetViewWidth(max_child_size.width); + result.width = available_size.width; + } + if (IsVerticalScrollEnabled()) + { + SetViewHeight(max_child_size.height); + result.height = available_size.height; + } + + return result; + } + + void ScrollControl::OnLayoutContent(const Rect& rect) + { + auto layout_rect = rect; + + if (IsHorizontalScrollEnabled()) + layout_rect.width = GetViewWidth(); + if (IsVerticalScrollEnabled()) + layout_rect.height = GetViewHeight(); + + for (auto control: GetChildren()) + { + const auto size = control->GetDesiredSize(); + // Ignore alignment, always center aligned. + auto&& calculate_anchor = [](const float anchor, const float layout_length, const float control_length, const float offset) -> float + { + return anchor + (layout_length - control_length) / 2 - offset; + }; + + control->Layout(Rect(Point( + calculate_anchor(rect.left, layout_rect.width, size.width, offset_x_), + calculate_anchor(rect.top, layout_rect.height, size.height, offset_y_) + ), size)); + } + } + + void ScrollControl::AfterLayoutSelf() + { + UpdateScrollBarBorderInfo(); + CoerceAndSetOffsets(offset_x_, offset_y_, false); + UpdateScrollBarVisibility(); + } + + void ScrollControl::OnDrawForeground(ID2D1DeviceContext* device_context) + { + Control::OnDrawForeground(device_context); + + const auto predefined = UiManager::GetInstance()->GetPredefineResources(); + + if (is_horizontal_scroll_bar_visible_) + { + device_context->FillRectangle( + Convert(horizontal_bar_info_.border), + predefined->scroll_bar_background_brush.Get() + ); + + device_context->FillRectangle( + Convert(horizontal_bar_info_.bar), + predefined->scroll_bar_brush.Get() + ); + + device_context->DrawLine( + Convert(horizontal_bar_info_.border.GetLeftTop()), + Convert(horizontal_bar_info_.border.GetRightTop()), + predefined->scroll_bar_border_brush.Get() + ); + } + + if (is_vertical_scroll_bar_visible_) + { + device_context->FillRectangle( + Convert(vertical_bar_info_.border), + predefined->scroll_bar_background_brush.Get() + ); + + device_context->FillRectangle( + Convert(vertical_bar_info_.bar), + predefined->scroll_bar_brush.Get() + ); + + device_context->DrawLine( + Convert(vertical_bar_info_.border.GetLeftTop()), + Convert(vertical_bar_info_.border.GetLeftBottom()), + predefined->scroll_bar_border_brush.Get() + ); + } + } + + void ScrollControl::OnMouseDownCore(events::MouseButtonEventArgs& args) + { + Control::OnMouseDownCore(args); + + if (args.GetMouseButton() == MouseButton::Left) + { + const auto point = args.GetPoint(this); + if (is_vertical_scroll_bar_visible_ && vertical_bar_info_.bar.IsPointInside(point)) + { + GetWindow()->CaptureMouseFor(this); + is_pressing_scroll_bar_ = Orientation::Vertical; + pressing_delta_ = point.y - vertical_bar_info_.bar.top; + return; + } + + if (is_horizontal_scroll_bar_visible_ && horizontal_bar_info_.bar.IsPointInside(point)) + { + GetWindow()->CaptureMouseFor(this); + pressing_delta_ = point.x - horizontal_bar_info_.bar.left; + is_pressing_scroll_bar_ = Orientation::Horizontal; + return; + } + } + } + + void ScrollControl::OnMouseMoveCore(events::MouseEventArgs& args) + { + Control::OnMouseMoveCore(args); + + const auto mouse_point = args.GetPoint(this); + + if (is_pressing_scroll_bar_ == Orientation::Horizontal) + { + const auto new_head_position = mouse_point.x - pressing_delta_; + const auto new_offset = new_head_position / horizontal_bar_info_.border.width * view_width_; + SetScrollOffset(new_offset, std::nullopt); + return; + } + + if (is_pressing_scroll_bar_ == Orientation::Vertical) + { + const auto new_head_position = mouse_point.y - pressing_delta_; + const auto new_offset = new_head_position / vertical_bar_info_.border.height * view_height_; + SetScrollOffset(std::nullopt, new_offset); + return; + } + } + + void ScrollControl::OnMouseUpCore(events::MouseButtonEventArgs& args) + { + Control::OnMouseUpCore(args); + + if (args.GetMouseButton() == MouseButton::Left && is_pressing_scroll_bar_.has_value()) + { + GetWindow()->ReleaseCurrentMouseCapture(); + is_pressing_scroll_bar_ = std::nullopt; + } + } + + void ScrollControl::OnMouseWheelCore(events::MouseWheelEventArgs& args) + { + Control::OnMouseWheelCore(args); + + constexpr const auto view_delta = 30.0f; + + if (args.GetDelta() == 0.0f) + return; + + const auto content_rect = GetRect(RectRange::Content); + if (IsVerticalScrollEnabled() && GetScrollOffsetY() != (args.GetDelta() > 0.0f ? 0.0f : AtLeast0(GetViewHeight() - content_rect.height))) + { + SetScrollOffset(std::nullopt, GetScrollOffsetY() - args.GetDelta() / WHEEL_DELTA * view_delta); + return; + } + + if (IsHorizontalScrollEnabled() && GetScrollOffsetX() != (args.GetDelta() > 0.0f ? 0.0f : AtLeast0(GetViewWidth() - content_rect.width))) + { + SetScrollOffset(GetScrollOffsetX() - args.GetDelta() / WHEEL_DELTA * view_delta, std::nullopt); + return; + } + } + + void ScrollControl::CoerceAndSetOffsets(const float offset_x, const float offset_y, const bool update_children) + { + const auto old_offset_x = offset_x_; + const auto old_offset_y = offset_y_; + + const auto content_rect = GetRect(RectRange::Content); + offset_x_ = Coerce(offset_x, 0.0f, AtLeast0(view_width_ - content_rect.width)); + offset_y_ = Coerce(offset_y, 0.0f, AtLeast0(view_height_ - content_rect.height)); + UpdateScrollBarBarInfo(); + + if (update_children) + { + for (auto child : GetChildren()) + { + const auto old_position = child->GetPositionRelative(); + child->SetPositionRelative(Point( + old_position.x + old_offset_x - offset_x_, + old_position.y + old_offset_y - offset_y_ + )); + } + } + InvalidateDraw(); + } + + void ScrollControl::UpdateScrollBarVisibility() + { + const auto content_rect = GetRect(RectRange::Content); + if (GetHorizontalScrollBarVisibility() == ScrollBarVisibility::Auto) + is_horizontal_scroll_bar_visible_ = view_width_ > content_rect.width; + if (GetVerticalScrollBarVisibility() == ScrollBarVisibility::Auto) + is_vertical_scroll_bar_visible_ = view_height_ > content_rect.height; + } + + void ScrollControl::UpdateScrollBarBorderInfo() + { + const auto content_rect = GetRect(RectRange::Content); + horizontal_bar_info_.border = Rect(content_rect.left, content_rect.GetBottom() - scroll_bar_width, content_rect.width, scroll_bar_width); + vertical_bar_info_.border = Rect(content_rect.GetRight() - scroll_bar_width , content_rect.top, scroll_bar_width, content_rect.height); + } + + void ScrollControl::UpdateScrollBarBarInfo() + { + const auto content_rect = GetRect(RectRange::Content); + { + const auto& border = horizontal_bar_info_.border; + if (view_width_ <= content_rect.width) + horizontal_bar_info_.bar = border; + else + { + const auto bar_length = border.width * content_rect.width / view_width_; + const auto offset = border.width * offset_x_ / view_width_; + horizontal_bar_info_.bar = Rect(border.left + offset, border.top, bar_length, border.height); + } + } + { + const auto& border = vertical_bar_info_.border; + if (view_height_ <= content_rect.height) + vertical_bar_info_.bar = border; + else + { + const auto bar_length = border.height * content_rect.height / view_height_; + const auto offset = border.height * offset_y_ / view_height_; + vertical_bar_info_.bar = Rect(border.left, border.top + offset, border.width, bar_length); + } + } + } +} diff --git a/src/ui/controls/scroll_control.hpp b/src/ui/controls/scroll_control.hpp new file mode 100644 index 00000000..0541a010 --- /dev/null +++ b/src/ui/controls/scroll_control.hpp @@ -0,0 +1,161 @@ +#pragma once + +// ReSharper disable once CppUnusedIncludeDirective +#include "pre.hpp" + +#include <optional> +#include <initializer_list> + +#include "ui/control.hpp" + +namespace cru::ui::controls +{ + // Done: OnMeasureContent + // Done: OnLayoutContent + // Done: HitTest(no need) + // Done: Draw(no need) + // Done: API + // Done: ScrollBar + // Done: MouseEvent + class ScrollControl : public Control + { + private: + struct ScrollBarInfo + { + Rect border = Rect(); + Rect bar = Rect(); + }; + + enum class Orientation + { + Horizontal, + Vertical + }; + + public: + enum class ScrollBarVisibility + { + None, + Auto, + Always + }; + + static ScrollControl* Create(const std::initializer_list<Control*>& children = std::initializer_list<Control*>{}) + { + const auto control = new ScrollControl(true); + for (auto child : children) + control->AddChild(child); + return control; + } + + static constexpr auto control_type = L"ScrollControl"; + + protected: + explicit ScrollControl(bool container); + public: + ScrollControl(const ScrollControl& other) = delete; + ScrollControl(ScrollControl&& other) = delete; + ScrollControl& operator=(const ScrollControl& other) = delete; + ScrollControl& operator=(ScrollControl&& other) = delete; + ~ScrollControl() override; + + StringView GetControlType() const override final; + + bool IsHorizontalScrollEnabled() const + { + return horizontal_scroll_enabled_; + } + + void SetHorizontalScrollEnabled(bool enable); + + bool IsVerticalScrollEnabled() const + { + return vertical_scroll_enabled_; + } + + void SetVerticalScrollEnabled(bool enable); + + + ScrollBarVisibility GetHorizontalScrollBarVisibility() const + { + return horizontal_scroll_bar_visibility_; + } + + void SetHorizontalScrollBarVisibility(ScrollBarVisibility visibility); + + ScrollBarVisibility GetVerticalScrollBarVisibility() const + { + return vertical_scroll_bar_visibility_; + } + + void SetVerticalScrollBarVisibility(ScrollBarVisibility visibility); + + float GetViewWidth() const + { + return view_width_; + } + + float GetViewHeight() const + { + return view_height_; + } + + float GetScrollOffsetX() const + { + return offset_x_; + } + + float GetScrollOffsetY() const + { + return offset_y_; + } + + // nullopt for not set. value is auto-coerced. + void SetScrollOffset(std::optional<float> x, std::optional<float> y); + + protected: + void SetViewWidth(float length); + void SetViewHeight(float length); + + Size OnMeasureContent(const Size& available_size) override final; + void OnLayoutContent(const Rect& rect) override final; + + void AfterLayoutSelf() override; + + void OnDrawForeground(ID2D1DeviceContext* device_context) override; + + void OnMouseDownCore(events::MouseButtonEventArgs& args) override final; + void OnMouseMoveCore(events::MouseEventArgs& args) override final; + void OnMouseUpCore(events::MouseButtonEventArgs& args) override final; + + void OnMouseWheelCore(events::MouseWheelEventArgs& args) override; + + private: + void CoerceAndSetOffsets(float offset_x, float offset_y, bool update_children = true); + void UpdateScrollBarVisibility(); + void UpdateScrollBarBorderInfo(); + void UpdateScrollBarBarInfo(); + + private: + bool horizontal_scroll_enabled_ = true; + bool vertical_scroll_enabled_ = true; + + ScrollBarVisibility horizontal_scroll_bar_visibility_ = ScrollBarVisibility::Auto; + ScrollBarVisibility vertical_scroll_bar_visibility_ = ScrollBarVisibility::Auto; + + bool is_horizontal_scroll_bar_visible_ = false; + bool is_vertical_scroll_bar_visible_ = false; + + float offset_x_ = 0.0f; + float offset_y_ = 0.0f; + + float view_width_ = 0.0f; + float view_height_ = 0.0f; + + ScrollBarInfo horizontal_bar_info_; + ScrollBarInfo vertical_bar_info_; + + std::optional<Orientation> is_pressing_scroll_bar_ = std::nullopt; + float pressing_delta_ = 0.0f; + }; +} diff --git a/src/ui/controls/text_block.hpp b/src/ui/controls/text_block.hpp index 4d017da5..66f5defa 100644 --- a/src/ui/controls/text_block.hpp +++ b/src/ui/controls/text_block.hpp @@ -1,5 +1,8 @@ #pragma once +// ReSharper disable once CppUnusedIncludeDirective +#include "pre.hpp" + #include "text_control.hpp" namespace cru::ui::controls diff --git a/src/ui/controls/text_box.hpp b/src/ui/controls/text_box.hpp index 65f81fc3..3a30ecb2 100644 --- a/src/ui/controls/text_box.hpp +++ b/src/ui/controls/text_box.hpp @@ -1,5 +1,8 @@ #pragma once +// ReSharper disable once CppUnusedIncludeDirective +#include "pre.hpp" + #include "text_control.hpp" #include "timer.hpp" diff --git a/src/ui/controls/text_control.cpp b/src/ui/controls/text_control.cpp index f7f88d4e..d7d6b810 100644 --- a/src/ui/controls/text_control.cpp +++ b/src/ui/controls/text_control.cpp @@ -19,6 +19,8 @@ namespace cru::ui::controls brush_ = init_brush; selection_brush_ = UiManager::GetInstance()->GetPredefineResources()->text_control_selection_brush; + + SetClipContent(true); } diff --git a/src/ui/controls/text_control.hpp b/src/ui/controls/text_control.hpp index 93120a44..762d85f3 100644 --- a/src/ui/controls/text_control.hpp +++ b/src/ui/controls/text_control.hpp @@ -1,5 +1,8 @@ #pragma once +// ReSharper disable once CppUnusedIncludeDirective +#include "pre.hpp" + #include "ui/control.hpp" namespace cru::ui::controls diff --git a/src/ui/controls/toggle_button.hpp b/src/ui/controls/toggle_button.hpp index 5de40ca5..4cbb4f37 100644 --- a/src/ui/controls/toggle_button.hpp +++ b/src/ui/controls/toggle_button.hpp @@ -1,5 +1,8 @@ #pragma once +// ReSharper disable once CppUnusedIncludeDirective +#include "pre.hpp" + #include "ui/control.hpp" namespace cru::ui::controls diff --git a/src/ui/convert_util.hpp b/src/ui/convert_util.hpp index 6deb7fff..5408f2e4 100644 --- a/src/ui/convert_util.hpp +++ b/src/ui/convert_util.hpp @@ -1,5 +1,8 @@ #pragma once +// ReSharper disable once CppUnusedIncludeDirective +#include "pre.hpp" + #include "system_headers.hpp" #include "ui_base.hpp" diff --git a/src/ui/cursor.cpp b/src/ui/cursor.cpp index cf88cd25..91b94b16 100644 --- a/src/ui/cursor.cpp +++ b/src/ui/cursor.cpp @@ -21,5 +21,12 @@ namespace cru::ui Cursor::Ptr arrow{}; Cursor::Ptr hand{}; Cursor::Ptr i_beam{}; + + void LoadSystemCursors() + { + arrow = std::make_shared<Cursor>(::LoadCursorW(nullptr, IDC_ARROW), false); + hand = std::make_shared<Cursor>(::LoadCursorW(nullptr, IDC_HAND), false); + i_beam = std::make_shared<Cursor>(::LoadCursorW(nullptr, IDC_IBEAM), false); + } } } diff --git a/src/ui/cursor.hpp b/src/ui/cursor.hpp index e3657171..273e524d 100644 --- a/src/ui/cursor.hpp +++ b/src/ui/cursor.hpp @@ -1,5 +1,8 @@ #pragma once +// ReSharper disable once CppUnusedIncludeDirective +#include "pre.hpp" + #include "system_headers.hpp" #include <memory> @@ -33,6 +36,8 @@ namespace cru::ui { extern Cursor::Ptr arrow; extern Cursor::Ptr hand; - extern Cursor::Ptr i_beam; + extern Cursor::Ptr i_beam; + + void LoadSystemCursors(); } } diff --git a/src/ui/events/ui_event.hpp b/src/ui/events/ui_event.hpp index c0585506..321e7135 100644 --- a/src/ui/events/ui_event.hpp +++ b/src/ui/events/ui_event.hpp @@ -1,5 +1,8 @@ #pragma once +// ReSharper disable once CppUnusedIncludeDirective +#include "pre.hpp" + #include "system_headers.hpp" #include <optional> @@ -85,6 +88,30 @@ namespace cru::ui::events }; + class MouseWheelEventArgs : public MouseEventArgs + { + public: + MouseWheelEventArgs(Object* sender, Object* original_sender, const Point& point, const float delta) + : MouseEventArgs(sender, original_sender, point), delta_(delta) + { + + } + MouseWheelEventArgs(const MouseWheelEventArgs& other) = default; + MouseWheelEventArgs(MouseWheelEventArgs&& other) = default; + MouseWheelEventArgs& operator=(const MouseWheelEventArgs& other) = default; + MouseWheelEventArgs& operator=(MouseWheelEventArgs&& other) = default; + ~MouseWheelEventArgs() override = default; + + float GetDelta() const + { + return delta_; + } + + private: + float delta_; + }; + + class DrawEventArgs : public UiEventArgs { public: @@ -304,6 +331,7 @@ namespace cru::ui::events using UiEvent = Event<UiEventArgs>; using MouseEvent = Event<MouseEventArgs>; using MouseButtonEvent = Event<MouseButtonEventArgs>; + using MouseWheelEvent = Event<MouseWheelEventArgs>; using DrawEvent = Event<DrawEventArgs>; using PositionChangedEvent = Event<PositionChangedEventArgs>; using SizeChangedEvent = Event<SizeChangedEventArgs>; diff --git a/src/ui/layout_base.hpp b/src/ui/layout_base.hpp index 7ae6f65c..2ae21837 100644 --- a/src/ui/layout_base.hpp +++ b/src/ui/layout_base.hpp @@ -1,5 +1,8 @@ #pragma once +// ReSharper disable once CppUnusedIncludeDirective +#include "pre.hpp" + #include <unordered_set> #include "base.hpp" diff --git a/src/ui/ui_base.hpp b/src/ui/ui_base.hpp index d9c9d0b2..b898b2ed 100644 --- a/src/ui/ui_base.hpp +++ b/src/ui/ui_base.hpp @@ -1,5 +1,8 @@ #pragma once +// ReSharper disable once CppUnusedIncludeDirective +#include "pre.hpp" + #include <optional> @@ -150,6 +153,16 @@ namespace cru::ui return Point(left + width, top + height); } + constexpr Point GetLeftBottom() const + { + return Point(left, top + height); + } + + constexpr Point GetRightTop() const + { + return Point(left + width, top); + } + constexpr Size GetSize() const { return Size(width, height); diff --git a/src/ui/ui_manager.cpp b/src/ui/ui_manager.cpp index 36fb2fb0..689a04a2 100644 --- a/src/ui/ui_manager.cpp +++ b/src/ui/ui_manager.cpp @@ -75,7 +75,11 @@ namespace cru::ui list_item_hover_border_brush {CreateSolidBrush(graph_manager, D2D1::ColorF(D2D1::ColorF::SkyBlue))}, list_item_hover_fill_brush {CreateSolidBrush(graph_manager, D2D1::ColorF(D2D1::ColorF::SkyBlue, 0.3f))}, list_item_select_border_brush {CreateSolidBrush(graph_manager, D2D1::ColorF(D2D1::ColorF::MediumBlue))}, - list_item_select_fill_brush {CreateSolidBrush(graph_manager, D2D1::ColorF(D2D1::ColorF::SkyBlue, 0.3f))} + list_item_select_fill_brush {CreateSolidBrush(graph_manager, D2D1::ColorF(D2D1::ColorF::SkyBlue, 0.3f))}, + + scroll_bar_background_brush {CreateSolidBrush(graph_manager, D2D1::ColorF(D2D1::ColorF::Gainsboro, 0.3f))}, + scroll_bar_border_brush {CreateSolidBrush(graph_manager, D2D1::ColorF(D2D1::ColorF::DimGray))}, + scroll_bar_brush {CreateSolidBrush(graph_manager, D2D1::ColorF(D2D1::ColorF::DimGray))} #ifdef CRU_DEBUG_LAYOUT , diff --git a/src/ui/ui_manager.hpp b/src/ui/ui_manager.hpp index 6b368e12..f0e1e8ce 100644 --- a/src/ui/ui_manager.hpp +++ b/src/ui/ui_manager.hpp @@ -1,5 +1,8 @@ #pragma once +// ReSharper disable once CppUnusedIncludeDirective +#include "pre.hpp" + #include "system_headers.hpp" #include "base.hpp" @@ -61,6 +64,10 @@ namespace cru::ui Microsoft::WRL::ComPtr<ID2D1Brush> list_item_select_border_brush; Microsoft::WRL::ComPtr<ID2D1Brush> list_item_select_fill_brush; + //region ScrollControl + Microsoft::WRL::ComPtr<ID2D1Brush> scroll_bar_background_brush; + Microsoft::WRL::ComPtr<ID2D1Brush> scroll_bar_border_brush; + Microsoft::WRL::ComPtr<ID2D1Brush> scroll_bar_brush; #ifdef CRU_DEBUG_LAYOUT //region debug diff --git a/src/ui/window.cpp b/src/ui/window.cpp index 87656cdc..9352b747 100644 --- a/src/ui/window.cpp +++ b/src/ui/window.cpp @@ -132,7 +132,8 @@ namespace cru::ui return new Window(tag_popup_constructor{}, parent, caption); } - Window::Window(tag_overlapped_constructor) : Control(WindowConstructorTag{}, this), control_list_({ this }) { + Window::Window(tag_overlapped_constructor) : Control(WindowConstructorTag{}, this) + { const auto window_manager = WindowManager::GetInstance(); hwnd_ = CreateWindowEx(0, @@ -148,7 +149,7 @@ namespace cru::ui AfterCreateHwnd(window_manager); } - Window::Window(tag_popup_constructor, Window* parent, const bool caption) : Control(WindowConstructorTag{}, this), control_list_({ this }) + Window::Window(tag_popup_constructor, Window* parent, const bool caption) : Control(WindowConstructorTag{}, this) { if (parent != nullptr && !parent->IsWindowValid()) throw std::runtime_error("Parent window is not valid."); @@ -411,6 +412,14 @@ namespace cru::ui result = 0; return true; } + case WM_MOUSEWHEEL: + POINT point; + point.x = GET_X_LPARAM(l_param); + point.y = GET_Y_LPARAM(l_param); + ScreenToClient(hwnd, &point); + OnMouseWheelInternal(GET_WHEEL_DELTA_WPARAM(w_param), point); + result = 0; + return true; case WM_KEYDOWN: OnKeyDownInternal(static_cast<int>(w_param)); result = 0; @@ -506,24 +515,6 @@ namespace cru::ui is_layout_invalid_ = false; } - void Window::RefreshControlList() { - control_list_.clear(); - TraverseDescendants([this](Control* control) { - this->control_list_.push_back(control); - }); - } - - Control * Window::HitTest(const Point & point) - { - for (auto i = control_list_.crbegin(); i != control_list_.crend(); ++i) { - auto control = *i; - if (control->IsPointInside(control->WindowToControl(point))) { - return control; - } - } - return nullptr; - } - bool Window::RequestFocusFor(Control * control) { if (control == nullptr) @@ -739,6 +730,20 @@ namespace cru::ui DispatchEvent(control, &Control::RaiseMouseUpEvent, nullptr, dip_point, button); } + void Window::OnMouseWheelInternal(short delta, POINT point) + { + const auto dip_point = PiToDip(point); + + Control* control; + + if (mouse_capture_control_) + control = mouse_capture_control_; + else + control = HitTest(dip_point); + + DispatchEvent(control, &Control::RaiseMouseWheelEvent, nullptr, dip_point, static_cast<float>(delta)); + } + void Window::OnKeyDownInternal(int virtual_code) { DispatchEvent(focus_control_, &Control::RaiseKeyDownEvent, nullptr, virtual_code); diff --git a/src/ui/window.hpp b/src/ui/window.hpp index d98e60e2..e82aa585 100644 --- a/src/ui/window.hpp +++ b/src/ui/window.hpp @@ -1,8 +1,10 @@ #pragma once +// ReSharper disable once CppUnusedIncludeDirective +#include "pre.hpp" + #include "system_headers.hpp" #include <map> -#include <list> #include <memory> #include "control.hpp" @@ -208,15 +210,6 @@ namespace cru::ui void SetSizeFitContent(const Size& max_size = Size(1000, 1000)); - //*************** region: functions *************** - - //Refresh control list. - //It should be invoked every time a control is added or removed from the tree. - void RefreshControlList(); - - //Get the most top control at "point". - Control* HitTest(const Point& point); - //*************** region: focus *************** @@ -277,7 +270,8 @@ namespace cru::ui void OnMouseLeaveInternal(); void OnMouseDownInternal(MouseButton button, POINT point); void OnMouseUpInternal(MouseButton button, POINT point); - + + void OnMouseWheelInternal(short delta, POINT point); void OnKeyDownInternal(int virtual_code); void OnKeyUpInternal(int virtual_code); void OnCharInternal(wchar_t c); @@ -320,8 +314,6 @@ namespace cru::ui Window* parent_window_ = nullptr; std::shared_ptr<graph::WindowRenderTarget> render_target_{}; - std::list<Control*> control_list_{}; - Control* mouse_hover_control_ = nullptr; bool window_focus_ = false; |