mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-05-23 19:42:53 +00:00
If we don't have enough layers to be able to scroll, the layers are pushed to be at the top of the layer list. This doesn't make much sense now that we are correctly drawing the layers in the right order, so now we draw them justified towards the bottom. Previously we were also clipping the bottom gadget slightly when there were enough layers to scroll. Now, I'm adding some offset to the total height to account for this and give equivalent spacing from the top and bottom layers.
361 lines
11 KiB
C++
361 lines
11 KiB
C++
/*
|
|
* Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
|
|
* Copyright (c) 2021, Mustafa Quraish <mustafa@cs.toronto.edu>
|
|
*
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
*/
|
|
|
|
#include "LayerListWidget.h"
|
|
#include "Image.h"
|
|
#include "ImageEditor.h"
|
|
#include "Layer.h"
|
|
#include <LibGUI/Painter.h>
|
|
#include <LibGfx/Palette.h>
|
|
|
|
REGISTER_WIDGET(PixelPaint, LayerListWidget);
|
|
|
|
namespace PixelPaint {
|
|
|
|
LayerListWidget::LayerListWidget()
|
|
{
|
|
set_should_hide_unnecessary_scrollbars(false);
|
|
horizontal_scrollbar().set_visible(false);
|
|
}
|
|
|
|
LayerListWidget::~LayerListWidget()
|
|
{
|
|
if (m_image)
|
|
m_image->remove_client(*this);
|
|
}
|
|
|
|
size_t LayerListWidget::to_gadget_index(size_t layer_index) const
|
|
{
|
|
return m_image->layer_count() - layer_index - 1;
|
|
}
|
|
|
|
size_t LayerListWidget::to_layer_index(size_t gadget_index) const
|
|
{
|
|
return m_image->layer_count() - gadget_index - 1;
|
|
}
|
|
|
|
void LayerListWidget::set_image(Image* image)
|
|
{
|
|
if (m_image == image)
|
|
return;
|
|
if (m_image)
|
|
m_image->remove_client(*this);
|
|
m_image = image;
|
|
if (m_image)
|
|
m_image->add_client(*this);
|
|
|
|
rebuild_gadgets();
|
|
}
|
|
|
|
void LayerListWidget::rebuild_gadgets()
|
|
{
|
|
m_gadgets.clear();
|
|
if (m_image) {
|
|
for (int layer_index = m_image->layer_count() - 1; layer_index >= 0; --layer_index) {
|
|
m_gadgets.append({ static_cast<size_t>(layer_index), {}, false, {} });
|
|
}
|
|
}
|
|
relayout_gadgets();
|
|
}
|
|
|
|
void LayerListWidget::resize_event(GUI::ResizeEvent& event)
|
|
{
|
|
AbstractScrollableWidget::resize_event(event);
|
|
relayout_gadgets();
|
|
}
|
|
|
|
void LayerListWidget::get_gadget_rects(Gadget const& gadget, Gfx::IntRect& outer_rect, Gfx::IntRect& thumbnail_rect, Gfx::IntRect& text_rect)
|
|
{
|
|
outer_rect = gadget.rect;
|
|
outer_rect.translate_by(0, -vertical_scrollbar().value());
|
|
outer_rect.translate_by(frame_thickness(), frame_thickness());
|
|
if (gadget.is_moving) {
|
|
outer_rect.translate_by(0, gadget.movement_delta.y());
|
|
}
|
|
|
|
thumbnail_rect = { outer_rect.x(), outer_rect.y(), outer_rect.height(), outer_rect.height() };
|
|
thumbnail_rect.shrink(8, 8);
|
|
|
|
text_rect = { thumbnail_rect.right() + 10, outer_rect.y(), outer_rect.width(), outer_rect.height() };
|
|
text_rect.intersect(outer_rect);
|
|
}
|
|
|
|
void LayerListWidget::paint_event(GUI::PaintEvent& event)
|
|
{
|
|
GUI::Painter painter(*this);
|
|
painter.add_clip_rect(event.rect());
|
|
|
|
painter.fill_rect(event.rect(), palette().button());
|
|
|
|
if (!m_image)
|
|
return;
|
|
|
|
painter.fill_rect(event.rect(), palette().button());
|
|
|
|
auto paint_gadget = [&](auto& gadget) {
|
|
auto& layer = m_image->layer(gadget.layer_index);
|
|
|
|
Gfx::IntRect adjusted_rect;
|
|
Gfx::IntRect thumbnail_rect;
|
|
Gfx::IntRect text_rect;
|
|
get_gadget_rects(gadget, adjusted_rect, thumbnail_rect, text_rect);
|
|
|
|
if (gadget.is_moving) {
|
|
painter.fill_rect(adjusted_rect, palette().selection().lightened(1.5f));
|
|
} else if (layer.is_selected()) {
|
|
painter.fill_rect(adjusted_rect, palette().selection());
|
|
}
|
|
|
|
painter.draw_rect(adjusted_rect, palette().color(ColorRole::BaseText));
|
|
painter.draw_scaled_bitmap(thumbnail_rect, layer.bitmap(), layer.bitmap().rect());
|
|
|
|
if (layer.is_visible()) {
|
|
painter.draw_text(text_rect, layer.name(), Gfx::TextAlignment::CenterLeft, layer.is_selected() ? palette().selection_text() : palette().button_text());
|
|
painter.draw_rect(thumbnail_rect, palette().color(ColorRole::BaseText));
|
|
} else {
|
|
painter.draw_text(text_rect, layer.name(), Gfx::TextAlignment::CenterLeft, palette().color(ColorRole::DisabledText));
|
|
painter.draw_rect(thumbnail_rect, palette().color(ColorRole::DisabledText));
|
|
}
|
|
};
|
|
|
|
for (auto& gadget : m_gadgets) {
|
|
if (!gadget.is_moving)
|
|
paint_gadget(gadget);
|
|
}
|
|
|
|
if (m_moving_gadget_index.has_value())
|
|
paint_gadget(m_gadgets[m_moving_gadget_index.value()]);
|
|
|
|
Gfx::StylePainter::paint_frame(painter, rect(), palette(), Gfx::FrameShape::Box, Gfx::FrameShadow::Sunken, 2);
|
|
}
|
|
|
|
Optional<size_t> LayerListWidget::gadget_at(Gfx::IntPoint const& position)
|
|
{
|
|
for (size_t i = 0; i < m_gadgets.size(); ++i) {
|
|
if (m_gadgets[i].rect.contains(position))
|
|
return i;
|
|
}
|
|
return {};
|
|
}
|
|
|
|
void LayerListWidget::mousedown_event(GUI::MouseEvent& event)
|
|
{
|
|
if (!m_image)
|
|
return;
|
|
if (event.button() != GUI::MouseButton::Left)
|
|
return;
|
|
|
|
Gfx::IntPoint translated_event_point = { 0, vertical_scrollbar().value() + event.y() };
|
|
|
|
auto maybe_gadget_index = gadget_at(translated_event_point);
|
|
if (!maybe_gadget_index.has_value())
|
|
return;
|
|
auto gadget_index = maybe_gadget_index.value();
|
|
|
|
m_moving_gadget_index = gadget_index;
|
|
m_selected_gadget_index = gadget_index;
|
|
m_moving_event_origin = translated_event_point;
|
|
auto& gadget = m_gadgets[m_moving_gadget_index.value()];
|
|
auto& layer = m_image->layer(to_layer_index(gadget_index));
|
|
set_selected_layer(&layer);
|
|
gadget.is_moving = true;
|
|
gadget.movement_delta = {};
|
|
update();
|
|
}
|
|
|
|
void LayerListWidget::mousemove_event(GUI::MouseEvent& event)
|
|
{
|
|
if (!m_image)
|
|
return;
|
|
if (!m_moving_gadget_index.has_value())
|
|
return;
|
|
|
|
Gfx::IntPoint translated_event_point = { 0, vertical_scrollbar().value() + event.y() };
|
|
|
|
auto delta = translated_event_point - m_moving_event_origin;
|
|
auto& gadget = m_gadgets[m_moving_gadget_index.value()];
|
|
VERIFY(gadget.is_moving);
|
|
gadget.movement_delta = delta;
|
|
|
|
auto adjusted_rect = gadget.rect;
|
|
adjusted_rect.translate_by(gadget.movement_delta);
|
|
scroll_into_view(adjusted_rect, false, true);
|
|
|
|
relayout_gadgets();
|
|
}
|
|
|
|
void LayerListWidget::mouseup_event(GUI::MouseEvent& event)
|
|
{
|
|
if (!m_image)
|
|
return;
|
|
if (event.button() != GUI::MouseButton::Left)
|
|
return;
|
|
if (!m_moving_gadget_index.has_value())
|
|
return;
|
|
|
|
size_t old_index = m_moving_gadget_index.value();
|
|
size_t new_index = hole_index_during_move();
|
|
if (new_index >= m_image->layer_count())
|
|
new_index = m_image->layer_count() - 1;
|
|
|
|
m_moving_gadget_index = {};
|
|
|
|
auto old_layer_index = to_layer_index(old_index);
|
|
auto new_layer_index = to_layer_index(new_index);
|
|
m_image->change_layer_index(old_layer_index, new_layer_index);
|
|
}
|
|
|
|
void LayerListWidget::context_menu_event(GUI::ContextMenuEvent& event)
|
|
{
|
|
Gfx::IntPoint translated_event_point = { 0, vertical_scrollbar().value() + event.position().y() };
|
|
|
|
auto gadget_index = gadget_at(translated_event_point);
|
|
if (gadget_index.has_value()) {
|
|
m_selected_gadget_index = gadget_index.value();
|
|
auto& layer = m_image->layer(to_layer_index(m_selected_gadget_index));
|
|
set_selected_layer(&layer);
|
|
}
|
|
|
|
if (on_context_menu_request)
|
|
on_context_menu_request(event);
|
|
}
|
|
|
|
void LayerListWidget::image_did_add_layer(size_t layer_index)
|
|
{
|
|
if (m_moving_gadget_index.has_value()) {
|
|
m_gadgets[m_moving_gadget_index.value()].is_moving = false;
|
|
m_moving_gadget_index = {};
|
|
}
|
|
auto gadget_index = to_gadget_index(layer_index);
|
|
Gadget gadget { gadget_index, {}, false, {} };
|
|
m_gadgets.insert(gadget_index, gadget);
|
|
relayout_gadgets();
|
|
}
|
|
|
|
void LayerListWidget::image_did_remove_layer(size_t layer_index)
|
|
{
|
|
if (m_moving_gadget_index.has_value()) {
|
|
m_gadgets[m_moving_gadget_index.value()].is_moving = false;
|
|
m_moving_gadget_index = {};
|
|
}
|
|
// No -1 here since a layer has already been removed.
|
|
auto gadget_index = m_image->layer_count() - layer_index;
|
|
m_gadgets.remove(gadget_index);
|
|
m_selected_gadget_index = to_gadget_index(0);
|
|
relayout_gadgets();
|
|
}
|
|
|
|
void LayerListWidget::image_did_modify_layer_properties(size_t layer_index)
|
|
{
|
|
update(m_gadgets[to_gadget_index(layer_index)].rect);
|
|
}
|
|
|
|
void LayerListWidget::image_did_modify_layer_bitmap(size_t layer_index)
|
|
{
|
|
Gfx::IntRect adjusted_rect;
|
|
Gfx::IntRect thumbnail_rect;
|
|
Gfx::IntRect text_rect;
|
|
get_gadget_rects(m_gadgets[to_gadget_index(layer_index)], adjusted_rect, thumbnail_rect, text_rect);
|
|
update(thumbnail_rect);
|
|
}
|
|
|
|
void LayerListWidget::image_did_modify_layer_stack()
|
|
{
|
|
rebuild_gadgets();
|
|
}
|
|
|
|
static constexpr int gadget_height = 40;
|
|
static constexpr int gadget_spacing = -1;
|
|
static constexpr int vertical_step = gadget_height + gadget_spacing;
|
|
|
|
size_t LayerListWidget::hole_index_during_move() const
|
|
{
|
|
VERIFY(is_moving_gadget());
|
|
auto& moving_gadget = m_gadgets[m_moving_gadget_index.value()];
|
|
int center_y_of_moving_gadget = moving_gadget.rect.translated(0, moving_gadget.movement_delta.y()).center().y();
|
|
return center_y_of_moving_gadget / vertical_step;
|
|
}
|
|
|
|
void LayerListWidget::select_bottom_layer()
|
|
{
|
|
if (!m_image || !m_image->layer_count())
|
|
return;
|
|
m_selected_gadget_index = to_gadget_index(0);
|
|
set_selected_layer(&m_image->layer(0));
|
|
}
|
|
|
|
void LayerListWidget::select_top_layer()
|
|
{
|
|
if (!m_image || !m_image->layer_count())
|
|
return;
|
|
m_selected_gadget_index = 0;
|
|
set_selected_layer(&m_image->layer(to_layer_index(0)));
|
|
}
|
|
|
|
void LayerListWidget::cycle_through_selection(int delta)
|
|
{
|
|
if (!m_image || !m_image->layer_count())
|
|
return;
|
|
|
|
auto current_index = static_cast<int>(m_selected_gadget_index);
|
|
current_index += delta;
|
|
|
|
if (current_index < 0)
|
|
current_index = m_gadgets.size() - 1;
|
|
if (current_index > static_cast<int>(m_gadgets.size()) - 1)
|
|
current_index = 0;
|
|
|
|
m_selected_gadget_index = current_index;
|
|
auto selected_layer_index = to_layer_index(m_selected_gadget_index);
|
|
set_selected_layer(&m_image->layer(selected_layer_index));
|
|
}
|
|
|
|
void LayerListWidget::relayout_gadgets()
|
|
{
|
|
auto total_gadget_height = static_cast<int>(m_gadgets.size()) * vertical_step + 6;
|
|
int y = max(0, height() - total_gadget_height);
|
|
|
|
Optional<size_t> hole_index;
|
|
if (is_moving_gadget())
|
|
hole_index = hole_index_during_move();
|
|
|
|
size_t index = 0;
|
|
for (auto& gadget : m_gadgets) {
|
|
if (gadget.is_moving)
|
|
continue;
|
|
if (index == hole_index)
|
|
y += vertical_step;
|
|
gadget.rect = { 0, y, widget_inner_rect().width(), gadget_height };
|
|
y += vertical_step;
|
|
++index;
|
|
}
|
|
|
|
set_content_size({ widget_inner_rect().width(), total_gadget_height });
|
|
vertical_scrollbar().set_range(0, max(total_gadget_height - height(), 0));
|
|
update();
|
|
}
|
|
|
|
void LayerListWidget::set_selected_layer(Layer* layer)
|
|
{
|
|
if (!m_image)
|
|
return;
|
|
for (size_t i = 0; i < m_image->layer_count(); ++i) {
|
|
if (layer == &m_image->layer(i)) {
|
|
m_image->layer(i).set_selected(true);
|
|
m_selected_gadget_index = to_gadget_index(i);
|
|
scroll_into_view(m_gadgets[m_selected_gadget_index].rect, false, true);
|
|
} else {
|
|
m_image->layer(i).set_selected(false);
|
|
}
|
|
}
|
|
if (on_layer_select)
|
|
on_layer_select(layer);
|
|
|
|
update();
|
|
}
|
|
|
|
}
|