mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2025-06-05 22:54:01 +02:00
Add Back button to large video tiles.
This commit is contained in:
parent
090d7d7112
commit
8bde53cd0f
12 changed files with 220 additions and 44 deletions
BIN
Telegram/Resources/icons/calls/video_back.png
Normal file
BIN
Telegram/Resources/icons/calls/video_back.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 294 B |
BIN
Telegram/Resources/icons/calls/video_back@2x.png
Normal file
BIN
Telegram/Resources/icons/calls/video_back@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 532 B |
BIN
Telegram/Resources/icons/calls/video_back@3x.png
Normal file
BIN
Telegram/Resources/icons/calls/video_back@3x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 790 B |
|
@ -1202,6 +1202,7 @@ GroupCallVideoTile {
|
|||
pinPosition: point;
|
||||
pinPadding: margins;
|
||||
pinTextPosition: point;
|
||||
back: icon;
|
||||
iconPosition: point;
|
||||
}
|
||||
|
||||
|
@ -1219,6 +1220,7 @@ groupCallVideoTile: GroupCallVideoTile {
|
|||
pinPosition: point(18px, 18px);
|
||||
pinPadding: margins(6px, 2px, 12px, 1px);
|
||||
pinTextPosition: point(1px, 3px);
|
||||
back: icon {{ "calls/video_back", groupCallVideoTextFg }};
|
||||
iconPosition: point(10px, 5px);
|
||||
}
|
||||
|
||||
|
|
|
@ -1187,8 +1187,19 @@ base::unique_qptr<Ui::PopupMenu> Members::Controller::createRowContextMenu(
|
|||
});
|
||||
|
||||
if (const auto real = _call->lookupReal()) {
|
||||
auto oneFound = false;
|
||||
auto hasTwoOrMore = false;
|
||||
for (const auto &[endpoint, track] : _call->activeVideoTracks()) {
|
||||
if (_call->shownVideoTracks().contains(endpoint)) {
|
||||
if (oneFound) {
|
||||
hasTwoOrMore = true;
|
||||
break;
|
||||
}
|
||||
oneFound = true;
|
||||
}
|
||||
}
|
||||
const auto participant = real->participantByPeer(participantPeer);
|
||||
if (participant) {
|
||||
if (participant && hasTwoOrMore) {
|
||||
const auto &large = _call->videoEndpointLarge();
|
||||
const auto pinned = _call->videoEndpointPinned();
|
||||
const auto &camera = computeCameraEndpoint(participant);
|
||||
|
|
|
@ -134,10 +134,10 @@ void Viewport::setMode(PanelMode mode, not_null<QWidget*> parent) {
|
|||
}
|
||||
if (!wide()) {
|
||||
for (const auto &tile : _tiles) {
|
||||
tile->togglePinShown(false);
|
||||
tile->toggleTopControlsShown(false);
|
||||
}
|
||||
} else if (_selected.tile) {
|
||||
_selected.tile->togglePinShown(true);
|
||||
_selected.tile->toggleTopControlsShown(true);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -154,7 +154,9 @@ void Viewport::handleMouseRelease(QPoint position, Qt::MouseButton button) {
|
|||
if (pressed == _selected) {
|
||||
if (button == Qt::RightButton) {
|
||||
tile->row()->showContextMenu();
|
||||
} else if (!wide()) {
|
||||
} else if (!wide()
|
||||
|| (_hasTwoOrMore && !_large)
|
||||
|| pressed.element == Selection::Element::BackButton) {
|
||||
_clicks.fire_copy(tile->endpoint());
|
||||
} else if (pressed.element == Selection::Element::PinButton) {
|
||||
_pinToggles.fire(!tile->pinned());
|
||||
|
@ -177,10 +179,14 @@ void Viewport::updateSelected(QPoint position) {
|
|||
if (geometry.contains(position)) {
|
||||
const auto pin = wide()
|
||||
&& tile->pinOuter().contains(position - geometry.topLeft());
|
||||
const auto back = wide()
|
||||
&& tile->backOuter().contains(position - geometry.topLeft());
|
||||
setSelected({
|
||||
.tile = tile.get(),
|
||||
.element = (pin
|
||||
? Selection::Element::PinButton
|
||||
: back
|
||||
? Selection::Element::BackButton
|
||||
: Selection::Element::Tile),
|
||||
});
|
||||
return;
|
||||
|
@ -238,6 +244,7 @@ void Viewport::showLarge(const VideoEndpoint &endpoint) {
|
|||
const auto large = (i != end(_tiles)) ? i->get() : nullptr;
|
||||
if (_large != large) {
|
||||
_large = large;
|
||||
updateTopControlsVisibility();
|
||||
updateTilesGeometry();
|
||||
}
|
||||
}
|
||||
|
@ -263,12 +270,40 @@ void Viewport::updateTilesGeometry(int outerWidth) {
|
|||
|
||||
if (wide()) {
|
||||
updateTilesGeometryWide(outerWidth, outerHeight);
|
||||
refreshHasTwoOrMore();
|
||||
_fullHeight = 0;
|
||||
} else {
|
||||
updateTilesGeometryNarrow(outerWidth);
|
||||
}
|
||||
}
|
||||
|
||||
void Viewport::refreshHasTwoOrMore() {
|
||||
auto hasTwoOrMore = false;
|
||||
auto oneFound = false;
|
||||
for (const auto &tile : _tiles) {
|
||||
if (!tile->trackSize().isEmpty()) {
|
||||
if (oneFound) {
|
||||
hasTwoOrMore = true;
|
||||
break;
|
||||
}
|
||||
oneFound = true;
|
||||
}
|
||||
}
|
||||
if (_hasTwoOrMore == hasTwoOrMore) {
|
||||
return;
|
||||
}
|
||||
_hasTwoOrMore = hasTwoOrMore;
|
||||
updateCursor();
|
||||
updateTopControlsVisibility();
|
||||
}
|
||||
|
||||
void Viewport::updateTopControlsVisibility() {
|
||||
if (_selected.tile) {
|
||||
_selected.tile->toggleTopControlsShown(
|
||||
_hasTwoOrMore && wide() && _large && _large == _selected.tile);
|
||||
}
|
||||
}
|
||||
|
||||
void Viewport::updateTilesGeometryWide(int outerWidth, int outerHeight) {
|
||||
if (!outerHeight) {
|
||||
return;
|
||||
|
@ -485,14 +520,19 @@ void Viewport::setSelected(Selection value) {
|
|||
return;
|
||||
}
|
||||
if (_selected.tile) {
|
||||
_selected.tile->togglePinShown(false);
|
||||
_selected.tile->toggleTopControlsShown(false);
|
||||
}
|
||||
_selected = value;
|
||||
if (_selected.tile && wide()) {
|
||||
_selected.tile->togglePinShown(true);
|
||||
}
|
||||
updateTopControlsVisibility();
|
||||
updateCursor();
|
||||
}
|
||||
|
||||
void Viewport::updateCursor() {
|
||||
const auto pointer = _selected.tile
|
||||
&& (!wide() || _selected.element == Selection::Element::PinButton);
|
||||
&& (!wide()
|
||||
|| (_hasTwoOrMore && !_large)
|
||||
|| _selected.element == Selection::Element::PinButton
|
||||
|| _selected.element == Selection::Element::BackButton);
|
||||
widget()->setCursor(pointer ? style::cur_pointer : style::cur_default);
|
||||
}
|
||||
|
||||
|
|
|
@ -97,6 +97,7 @@ private:
|
|||
None,
|
||||
Tile,
|
||||
PinButton,
|
||||
BackButton,
|
||||
};
|
||||
VideoTile *tile = nullptr;
|
||||
Element element = Element::None;
|
||||
|
@ -111,11 +112,14 @@ private:
|
|||
void setup();
|
||||
[[nodiscard]] bool wide() const;
|
||||
|
||||
void updateCursor();
|
||||
void updateTilesGeometry();
|
||||
void updateTilesGeometry(int outerWidth);
|
||||
void updateTilesGeometryWide(int outerWidth, int outerHeight);
|
||||
void updateTilesGeometryNarrow(int outerWidth);
|
||||
void setTileGeometry(not_null<VideoTile*> tile, QRect geometry);
|
||||
void refreshHasTwoOrMore();
|
||||
void updateTopControlsVisibility();
|
||||
|
||||
void setSelected(Selection value);
|
||||
void setPressed(Selection value);
|
||||
|
@ -135,6 +139,7 @@ private:
|
|||
const std::unique_ptr<Ui::RpWidgetWrap> _content;
|
||||
std::vector<std::unique_ptr<VideoTile>> _tiles;
|
||||
rpl::variable<int> _fullHeight = 0;
|
||||
bool _hasTwoOrMore = false;
|
||||
int _scrollTop = 0;
|
||||
QImage _shadow;
|
||||
rpl::event_stream<VideoEndpoint> _clicks;
|
||||
|
|
|
@ -262,7 +262,7 @@ void Viewport::RendererGL::init(
|
|||
_frameBuffer->setUsagePattern(QOpenGLBuffer::DynamicDraw);
|
||||
_frameBuffer->create();
|
||||
_frameBuffer->bind();
|
||||
constexpr auto kQuads = 6;
|
||||
constexpr auto kQuads = 7;
|
||||
constexpr auto kQuadVertices = kQuads * 4;
|
||||
constexpr auto kQuadValues = kQuadVertices * 4;
|
||||
constexpr auto kValues = kQuadValues + 8; // Blur texture coordinates.
|
||||
|
@ -486,6 +486,13 @@ void Viewport::RendererGL::paintTile(
|
|||
geometry);
|
||||
const auto pinRect = transformRect(pin.geometry);
|
||||
|
||||
// Back.
|
||||
const auto back = _buttons.texturedRect(
|
||||
tile->backInner().translated(x, y),
|
||||
_back,
|
||||
geometry);
|
||||
const auto backRect = transformRect(back.geometry);
|
||||
|
||||
// Mute.
|
||||
const auto &icon = st::groupCallVideoCrossLine.icon;
|
||||
const auto iconLeft = x + width - st.iconPosition.x() - icon.width();
|
||||
|
@ -573,6 +580,19 @@ void Viewport::RendererGL::paintTile(
|
|||
pinRect.left(), pinRect.bottom(),
|
||||
pin.texture.left(), pin.texture.top(),
|
||||
|
||||
// Back button.
|
||||
backRect.left(), backRect.top(),
|
||||
back.texture.left(), back.texture.bottom(),
|
||||
|
||||
backRect.right(), backRect.top(),
|
||||
back.texture.right(), back.texture.bottom(),
|
||||
|
||||
backRect.right(), backRect.bottom(),
|
||||
back.texture.right(), back.texture.top(),
|
||||
|
||||
backRect.left(), backRect.bottom(),
|
||||
back.texture.left(), back.texture.top(),
|
||||
|
||||
// Mute icon.
|
||||
muteRect.left(), muteRect.top(),
|
||||
mute.texture.left(), mute.texture.bottom(),
|
||||
|
@ -683,6 +703,7 @@ void Viewport::RendererGL::paintTile(
|
|||
|
||||
if (pinVisible) {
|
||||
FillTexturedRectangle(f, &*_imageProgram, 14);
|
||||
FillTexturedRectangle(f, &*_imageProgram, 18);
|
||||
}
|
||||
|
||||
if (nameShift == fullNameShift) {
|
||||
|
@ -691,13 +712,13 @@ void Viewport::RendererGL::paintTile(
|
|||
|
||||
// Mute.
|
||||
if (!muteRect.empty()) {
|
||||
FillTexturedRectangle(f, &*_imageProgram, 18);
|
||||
FillTexturedRectangle(f, &*_imageProgram, 22);
|
||||
}
|
||||
|
||||
// Name.
|
||||
if (!nameRect.empty()) {
|
||||
_names.bind(f);
|
||||
FillTexturedRectangle(f, &*_imageProgram, 22);
|
||||
FillTexturedRectangle(f, &*_imageProgram, 26);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -905,15 +926,20 @@ void Viewport::RendererGL::ensureButtonsImage() {
|
|||
const auto factor = cIntRetinaFactor();
|
||||
const auto pinOnSize = VideoTile::PinInnerSize(true);
|
||||
const auto pinOffSize = VideoTile::PinInnerSize(false);
|
||||
const auto backSize = VideoTile::BackInnerSize();
|
||||
const auto muteSize = st::groupCallVideoCrossLine.icon.size();
|
||||
|
||||
const auto fullSize = QSize(
|
||||
std::max({
|
||||
pinOnSize.width(),
|
||||
pinOffSize.width(),
|
||||
backSize.width(),
|
||||
2 * muteSize.width(),
|
||||
}),
|
||||
pinOnSize.height() + pinOffSize.height() + muteSize.height());
|
||||
(pinOnSize.height()
|
||||
+ pinOffSize.height()
|
||||
+ backSize.height()
|
||||
+ muteSize.height()));
|
||||
const auto imageSize = fullSize * factor;
|
||||
auto image = _buttons.takeImage();
|
||||
if (image.size() != imageSize) {
|
||||
|
@ -947,7 +973,19 @@ void Viewport::RendererGL::ensureButtonsImage() {
|
|||
&_pinBackground,
|
||||
&_pinIcon);
|
||||
|
||||
const auto muteTop = pinOnSize.height() + pinOffSize.height();
|
||||
_back = QRect(
|
||||
QPoint(0, pinOnSize.height() + pinOffSize.height()) * factor,
|
||||
backSize * factor);
|
||||
VideoTile::PaintBackButton(
|
||||
p,
|
||||
0,
|
||||
pinOnSize.height() + pinOffSize.height(),
|
||||
fullSize.width(),
|
||||
&_pinBackground);
|
||||
|
||||
const auto muteTop = pinOnSize.height()
|
||||
+ pinOffSize.height()
|
||||
+ backSize.height();
|
||||
_muteOn = QRect(QPoint(0, muteTop) * factor, muteSize * factor);
|
||||
_muteIcon.paint(p, { 0, muteTop }, 1.);
|
||||
|
||||
|
|
|
@ -120,6 +120,7 @@ private:
|
|||
Ui::GL::Image _buttons;
|
||||
QRect _pinOn;
|
||||
QRect _pinOff;
|
||||
QRect _back;
|
||||
QRect _muteOn;
|
||||
QRect _muteOff;
|
||||
|
||||
|
|
|
@ -122,18 +122,28 @@ void Viewport::Renderer::paintTileControls(
|
|||
p.setClipRect(x, y, width, height);
|
||||
const auto guard = gsl::finally([&] { p.setClipping(false); });
|
||||
|
||||
// Pin.
|
||||
const auto wide = _owner->wide();
|
||||
if (wide) {
|
||||
const auto inner = tile->pinInner();
|
||||
// Pin.
|
||||
|
||||
const auto pinInner = tile->pinInner();
|
||||
VideoTile::PaintPinButton(
|
||||
p,
|
||||
tile->pinned(),
|
||||
x + inner.x(),
|
||||
y + inner.y(),
|
||||
x + pinInner.x(),
|
||||
y + pinInner.y(),
|
||||
_owner->widget()->width(),
|
||||
&_pinBackground,
|
||||
&_pinIcon);
|
||||
|
||||
// Back.
|
||||
const auto backInner = tile->backInner();
|
||||
VideoTile::PaintBackButton(
|
||||
p,
|
||||
x + backInner.x(),
|
||||
y + backInner.y(),
|
||||
_owner->widget()->width(),
|
||||
&_pinBackground);
|
||||
}
|
||||
|
||||
const auto shown = _owner->_controlsShownRatio;
|
||||
|
|
|
@ -31,19 +31,27 @@ Viewport::VideoTile::VideoTile(
|
|||
setup(std::move(pinned));
|
||||
}
|
||||
|
||||
QRect Viewport::VideoTile::pinInner() const {
|
||||
return _pinInner.translated(0, -pinSlide());
|
||||
}
|
||||
|
||||
QRect Viewport::VideoTile::pinOuter() const {
|
||||
return _pinOuter;
|
||||
}
|
||||
|
||||
int Viewport::VideoTile::pinSlide() const {
|
||||
QRect Viewport::VideoTile::pinInner() const {
|
||||
return _pinInner.translated(0, -topControlsSlide());
|
||||
}
|
||||
|
||||
QRect Viewport::VideoTile::backOuter() const {
|
||||
return _backOuter;
|
||||
}
|
||||
|
||||
QRect Viewport::VideoTile::backInner() const {
|
||||
return _backInner.translated(0, -topControlsSlide());
|
||||
}
|
||||
|
||||
int Viewport::VideoTile::topControlsSlide() const {
|
||||
return anim::interpolate(
|
||||
st::groupCallVideoTile.pinPosition.y() + _pinInner.height(),
|
||||
0,
|
||||
_pinShownAnimation.value(_pinShown ? 1. : 0.));
|
||||
_topControlsShownAnimation.value(_topControlsShown ? 1. : 0.));
|
||||
}
|
||||
|
||||
bool Viewport::VideoTile::screencast() const {
|
||||
|
@ -52,15 +60,15 @@ bool Viewport::VideoTile::screencast() const {
|
|||
|
||||
void Viewport::VideoTile::setGeometry(QRect geometry) {
|
||||
_geometry = geometry;
|
||||
updatePinnedGeometry();
|
||||
updateTopControlsGeometry();
|
||||
}
|
||||
|
||||
void Viewport::VideoTile::togglePinShown(bool shown) {
|
||||
if (_pinShown == shown) {
|
||||
void Viewport::VideoTile::toggleTopControlsShown(bool shown) {
|
||||
if (_topControlsShown == shown) {
|
||||
return;
|
||||
}
|
||||
_pinShown = shown;
|
||||
_pinShownAnimation.start(
|
||||
_topControlsShown = shown;
|
||||
_topControlsShownAnimation.start(
|
||||
_update,
|
||||
shown ? 0. : 1.,
|
||||
shown ? 1. : 0.,
|
||||
|
@ -128,19 +136,68 @@ void Viewport::VideoTile::PaintPinButton(
|
|||
|
||||
}
|
||||
|
||||
void Viewport::VideoTile::updatePinnedGeometry() {
|
||||
QSize Viewport::VideoTile::BackInnerSize() {
|
||||
const auto &st = st::groupCallVideoTile;
|
||||
const auto buttonSize = PinInnerSize(_pinned);
|
||||
const auto fullWidth = st.pinPosition.x() * 2 + buttonSize.width();
|
||||
const auto fullHeight = st.pinPosition.y() * 2 + buttonSize.height();
|
||||
_pinInner = QRect(QPoint(), buttonSize).translated(
|
||||
_geometry.width() - st.pinPosition.x() - buttonSize.width(),
|
||||
const auto &icon = st::groupCallVideoTile.back;
|
||||
const auto innerWidth = icon.width()
|
||||
+ st.pinTextPosition.x()
|
||||
+ st::semiboldFont->width(tr::lng_create_group_back(tr::now));
|
||||
const auto innerHeight = icon.height();
|
||||
const auto buttonWidth = st.pinPadding.left()
|
||||
+ innerWidth
|
||||
+ st.pinPadding.right();
|
||||
const auto buttonHeight = st.pinPadding.top()
|
||||
+ innerHeight
|
||||
+ st.pinPadding.bottom();
|
||||
return { buttonWidth, buttonHeight };
|
||||
}
|
||||
|
||||
void Viewport::VideoTile::PaintBackButton(
|
||||
Painter &p,
|
||||
int x,
|
||||
int y,
|
||||
int outerWidth,
|
||||
not_null<Ui::RoundRect*> background) {
|
||||
const auto &st = st::groupCallVideoTile;
|
||||
const auto rect = QRect(QPoint(x, y), BackInnerSize());
|
||||
background->paint(p, rect);
|
||||
st.back.paint(
|
||||
p,
|
||||
rect.marginsRemoved(st.pinPadding).topLeft(),
|
||||
outerWidth);
|
||||
p.setPen(st::groupCallVideoTextFg);
|
||||
p.setFont(st::semiboldFont);
|
||||
p.drawTextLeft(
|
||||
(x
|
||||
+ st.pinPadding.left()
|
||||
+ st::groupCallVideoTile.pin.icon.width()
|
||||
+ st.pinTextPosition.x()),
|
||||
(y
|
||||
+ st.pinPadding.top()
|
||||
+ st.pinTextPosition.y()),
|
||||
outerWidth,
|
||||
tr::lng_create_group_back(tr::now));
|
||||
}
|
||||
|
||||
void Viewport::VideoTile::updateTopControlsGeometry() {
|
||||
const auto &st = st::groupCallVideoTile;
|
||||
|
||||
const auto pinSize = PinInnerSize(_pinned);
|
||||
const auto pinWidth = st.pinPosition.x() * 2 + pinSize.width();
|
||||
const auto pinHeight = st.pinPosition.y() * 2 + pinSize.height();
|
||||
_pinInner = QRect(QPoint(), pinSize).translated(
|
||||
_geometry.width() - st.pinPosition.x() - pinSize.width(),
|
||||
st.pinPosition.y());
|
||||
_pinOuter = QRect(
|
||||
_geometry.width() - fullWidth,
|
||||
_geometry.width() - pinWidth,
|
||||
0,
|
||||
fullWidth,
|
||||
fullHeight);
|
||||
pinWidth,
|
||||
pinHeight);
|
||||
const auto backSize = BackInnerSize();
|
||||
const auto backWidth = st.pinPosition.x() * 2 + backSize.width();
|
||||
const auto backHeight = st.pinPosition.y() * 2 + backSize.height();
|
||||
_backInner = QRect(QPoint(), backSize).translated(st.pinPosition);
|
||||
_backOuter = QRect(0, 0, backWidth, backHeight);
|
||||
}
|
||||
|
||||
void Viewport::VideoTile::setup(rpl::producer<bool> pinned) {
|
||||
|
@ -150,7 +207,7 @@ void Viewport::VideoTile::setup(rpl::producer<bool> pinned) {
|
|||
return (_pinned != pinned);
|
||||
}) | rpl::start_with_next([=](bool pinned) {
|
||||
_pinned = pinned;
|
||||
updatePinnedGeometry();
|
||||
updateTopControlsGeometry();
|
||||
_update();
|
||||
}, _lifetime);
|
||||
|
||||
|
|
|
@ -43,6 +43,8 @@ public:
|
|||
}
|
||||
[[nodiscard]] QRect pinOuter() const;
|
||||
[[nodiscard]] QRect pinInner() const;
|
||||
[[nodiscard]] QRect backOuter() const;
|
||||
[[nodiscard]] QRect backInner() const;
|
||||
[[nodiscard]] const VideoEndpoint &endpoint() const {
|
||||
return _endpoint;
|
||||
}
|
||||
|
@ -55,7 +57,7 @@ public:
|
|||
|
||||
[[nodiscard]] bool screencast() const;
|
||||
void setGeometry(QRect geometry);
|
||||
void togglePinShown(bool shown);
|
||||
void toggleTopControlsShown(bool shown);
|
||||
bool updateRequestedQuality(VideoQuality quality);
|
||||
|
||||
[[nodiscard]] rpl::lifetime &lifetime() {
|
||||
|
@ -72,10 +74,18 @@ public:
|
|||
not_null<Ui::RoundRect*> background,
|
||||
not_null<Ui::CrossLineAnimation*> icon);
|
||||
|
||||
[[nodiscard]] static QSize BackInnerSize();
|
||||
static void PaintBackButton(
|
||||
Painter &p,
|
||||
int x,
|
||||
int y,
|
||||
int outerWidth,
|
||||
not_null<Ui::RoundRect*> background);
|
||||
|
||||
private:
|
||||
void setup(rpl::producer<bool> pinned);
|
||||
[[nodiscard]] int pinSlide() const;
|
||||
void updatePinnedGeometry();
|
||||
[[nodiscard]] int topControlsSlide() const;
|
||||
void updateTopControlsGeometry();
|
||||
|
||||
const VideoEndpoint _endpoint;
|
||||
const Fn<void()> _update;
|
||||
|
@ -85,8 +95,10 @@ private:
|
|||
rpl::variable<QSize> _trackSize;
|
||||
QRect _pinOuter;
|
||||
QRect _pinInner;
|
||||
Ui::Animations::Simple _pinShownAnimation;
|
||||
bool _pinShown = false;
|
||||
QRect _backOuter;
|
||||
QRect _backInner;
|
||||
Ui::Animations::Simple _topControlsShownAnimation;
|
||||
bool _topControlsShown = false;
|
||||
bool _pinned = false;
|
||||
std::optional<VideoQuality> _quality;
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue