diff --git a/CMakeLists.txt b/CMakeLists.txt index 9038739..6d03d8b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -252,6 +252,8 @@ PRIVATE ui/wrap/padding_wrap.h ui/wrap/slide_wrap.cpp ui/wrap/slide_wrap.h + ui/wrap/table_layout.cpp + ui/wrap/table_layout.h ui/wrap/vertical_layout.cpp ui/wrap/vertical_layout.h ui/wrap/vertical_layout_reorder.cpp diff --git a/ui/widgets/widgets.style b/ui/widgets/widgets.style index f9a93a2..8b90876 100644 --- a/ui/widgets/widgets.style +++ b/ui/widgets/widgets.style @@ -612,6 +612,16 @@ Toast { durationSlide: int; } +Table { + bg: color; + headerBg: color; + borderFg: color; + border: pixels; + radius: pixels; + labelMinWidth: pixels; + labelMaxWidth: double; +} + defaultLabelSimple: LabelSimple { font: normalFont; maxWidth: 0px; @@ -640,6 +650,15 @@ defaultRippleAnimation: RippleAnimation { hideDuration: 200; } +defaultTable: Table { + bg: windowBg; + headerBg: windowBgOver; + borderFg: inputBorderFg; + border: 1px; + radius: 6px; + labelMaxWidth: 0.5; +} + emptyRippleAnimation: RippleAnimation { } diff --git a/ui/wrap/table_layout.cpp b/ui/wrap/table_layout.cpp new file mode 100644 index 0000000..feecc1a --- /dev/null +++ b/ui/wrap/table_layout.cpp @@ -0,0 +1,235 @@ +// This file is part of Desktop App Toolkit, +// a set of libraries for developing nice desktop applications. +// +// For license and copyright information please follow this link: +// https://github.com/desktop-app/legal/blob/master/LEGAL +// +#include "ui/wrap/table_layout.h" + +#include "ui/painter.h" +#include "ui/ui_utility.h" +#include "styles/style_widgets.h" + +#include + +namespace Ui { + +TableLayout::TableLayout(QWidget *parent, const style::Table &st) +: RpWidget(parent) +, _st(st) { +} + +void TableLayout::paintEvent(QPaintEvent *e) { + if (_rows.empty()) { + return; + } + + auto p = QPainter(this); + auto hq = PainterHighQualityEnabler(p); + + const auto half = _st.border / 2.; + const auto inner = QRectF(rect()).marginsRemoved( + { half, half, half, half }); + + p.setClipRect(0, 0, _valueLeft, height()); + p.setBrush(_st.headerBg); + p.setPen(Qt::NoPen); + p.drawRoundedRect(inner, _st.radius, _st.radius); + p.setClipping(false); + + auto path = QPainterPath(); + path.addRoundedRect(inner, _st.radius, _st.radius); + auto top = half; + for (auto i = 1, count = int(_rows.size()); i != count; ++i) { + const auto y = _rows[i].top - half; + path.moveTo(half, y); + path.lineTo(width() - half, y); + } + path.moveTo(_valueLeft - half, half); + path.lineTo(_valueLeft - half, height() - half); + + auto pen = _st.borderFg->p; + pen.setWidth(_st.border); + p.setPen(pen); + p.setBrush(Qt::NoBrush); + p.drawPath(path); +} + +int TableLayout::resizeGetHeight(int newWidth) { + _inResize = true; + auto guard = gsl::finally([&] { _inResize = false; }); + + auto available = newWidth - 3 * _st.border; + const auto labelMax = int(base::SafeRound( + _st.labelMaxWidth * available)); + const auto valueMin = available - labelMax; + if (labelMax <= 0 || valueMin <= 0 || _rows.empty()) { + return 0; + } + auto label = _st.labelMinWidth; + for (auto &row : _rows) { + const auto natural = row.label->naturalWidth() + + row.labelMargin.left() + + row.labelMargin.right(); + if (natural < 0 || natural >= labelMax) { + label = labelMax; + break; + } else if (natural > label) { + label = natural; + } + } + _valueLeft = _st.border * 2 + label; + + auto result = _st.border; + for (auto &row : _rows) { + updateRowGeometry(row, newWidth, result); + result += rowVerticalSkip(row); + } + return result; +} + +void TableLayout::visibleTopBottomUpdated( + int visibleTop, + int visibleBottom) { + for (auto &row : _rows) { + setChildVisibleTopBottom( + row.label, + visibleTop, + visibleBottom); + setChildVisibleTopBottom( + row.value, + visibleTop, + visibleBottom); + } +} + +void TableLayout::updateRowGeometry( + const Row &row, + int width, + int top) const { + row.top = top; + row.label->resizeToNaturalWidth(_valueLeft + - 2 * _st.border + - row.labelMargin.left() + - row.labelMargin.right()); + row.label->moveToLeft( + _st.border + row.labelMargin.left(), + top + row.labelMargin.top(), + width); + row.value->resizeToNaturalWidth(width + - _valueLeft + - _st.border + - row.valueMargin.left() + - row.valueMargin.right()); + row.value->moveToLeft( + _valueLeft + row.valueMargin.left(), + top + row.valueMargin.top(), + width); +} + +void TableLayout::insertRow( + int atPosition, + object_ptr &&label, + object_ptr &&value, + const style::margins &labelMargin, + const style::margins &valueMargin) { + Expects(atPosition >= 0 && atPosition <= _rows.size()); + Expects(!_inResize); + + const auto wlabel = AttachParentChild(this, label); + const auto wvalue = AttachParentChild(this, value); + if (wlabel && wvalue) { + _rows.insert(begin(_rows) + atPosition, { + std::move(label), + std::move(value), + labelMargin, + valueMargin, + }); + wlabel->heightValue( + ) | rpl::start_with_next_done([=] { + if (!_inResize) { + childHeightUpdated(wlabel); + } + }, [=] { + removeChild(wlabel); + }, _rowsLifetime); + wvalue->heightValue( + ) | rpl::start_with_next_done([=] { + if (!_inResize) { + childHeightUpdated(wvalue); + } + }, [=] { + removeChild(wvalue); + }, _rowsLifetime); + } +} + +void TableLayout::childHeightUpdated(RpWidget *child) { + auto it = ranges::find_if(_rows, [child](const Row &row) { + return (row.label == child) || (row.value == child); + }); + + auto top = (it == _rows.begin()) ? _st.border : (it - 1)->top; + const auto outer = width(); + for (auto end = _rows.end(); it != end; ++it) { + const auto &row = *it; + row.top = top; + row.label->moveToLeft( + _st.border + row.labelMargin.left(), + top + row.labelMargin.top(), + outer); + row.value->moveToLeft( + _valueLeft + row.valueMargin.left(), + top + row.valueMargin.top(), + outer); + top += rowVerticalSkip(row); + } + resize(width(), _rows.empty() ? 0 : top); +} + +void TableLayout::removeChild(RpWidget *child) { + auto it = ranges::find_if(_rows, [child](const Row &row) { + return (row.label == child) || (row.value == child); + }); + auto end = _rows.end(); + Assert(it != end); + + auto top = (it == _rows.begin()) ? _st.border : (it - 1)->top; + const auto outer = width(); + for (auto next = it + 1; next != end; ++next) { + auto &row = *next; + row.top = top; + row.label->moveToLeft( + _st.border + row.labelMargin.left(), + top + row.labelMargin.top(), + outer); + row.value->moveToLeft( + _valueLeft + row.valueMargin.left(), + top + row.valueMargin.top(), + outer); + top += rowVerticalSkip(row); + } + it->label = nullptr; + it->value = nullptr; + _rows.erase(it); + + resize(width(), _rows.empty() ? 0 : top); +} + +int TableLayout::rowVerticalSkip(const Row &row) const { + const auto labelHeight = row.labelMargin.top() + + row.label->heightNoMargins() + + row.labelMargin.bottom(); + const auto valueHeight = row.valueMargin.top() + + row.value->heightNoMargins() + + row.valueMargin.bottom(); + return std::max(labelHeight, valueHeight) + _st.border; +} + +void TableLayout::clear() { + while (!_rows.empty()) { + removeChild(_rows.front().label.data()); + } +} + +} // namespace Ui diff --git a/ui/wrap/table_layout.h b/ui/wrap/table_layout.h new file mode 100644 index 0000000..eb1e3b4 --- /dev/null +++ b/ui/wrap/table_layout.h @@ -0,0 +1,95 @@ +// This file is part of Desktop App Toolkit, +// a set of libraries for developing nice desktop applications. +// +// For license and copyright information please follow this link: +// https://github.com/desktop-app/legal/blob/master/LEGAL +// +#pragma once + +#include "base/object_ptr.h" +#include "ui/rp_widget.h" + +namespace style { +struct Table; +} // namespace style + +namespace st { +extern const style::Table &defaultTable; +} // namespace st + +namespace Ui { + +class TableLayout : public RpWidget { +public: + TableLayout(QWidget *parent, const style::Table &st = st::defaultTable); + + [[nodiscard]] int rowsCount() const { + return _rows.size(); + } + + [[nodiscard]] not_null labelAt(int index) const { + Expects(index >= 0 && index < rowsCount()); + + return _rows[index].label.data(); + } + [[nodiscard]] not_null valueAt(int index) const { + Expects(index >= 0 && index < rowsCount()); + + return _rows[index].value.data(); + } + + void insertRow( + int atPosition, + object_ptr &&label, + object_ptr &&value, + const style::margins &labelMargin = style::margins(), + const style::margins &valueMargin = style::margins()); + + void addRow( + object_ptr &&label, + object_ptr &&value, + const style::margins &labelMargin = style::margins(), + const style::margins &valueMargin = style::margins()) { + insertRow( + rowsCount(), + std::move(label), + std::move(value), + labelMargin, + valueMargin); + } + + void clear(); + +protected: + void paintEvent(QPaintEvent *e) override; + + int resizeGetHeight(int newWidth) override; + void visibleTopBottomUpdated( + int visibleTop, + int visibleBottom) override; + +private: + struct Row { + object_ptr label; + object_ptr value; + style::margins labelMargin; + style::margins valueMargin; + mutable int top = 0; + }; + + [[nodiscard]] int rowVerticalSkip(const Row &row) const; + void childHeightUpdated(RpWidget *child); + void removeChild(RpWidget *child); + void updateRowGeometry(const Row &row, int width, int top) const; + + const style::Table &_st; + + std::vector _rows; + int _valueLeft = 0; + bool _inResize = false; + + rpl::lifetime _rowsLifetime; + +}; + +} // namespace Ui