LibGUI: Implement very bad widget system :D

This commit is contained in:
2025-06-27 13:49:15 +03:00
parent 4695fa061d
commit d73a667437
15 changed files with 1076 additions and 2 deletions

View File

@@ -0,0 +1,60 @@
#include <LibFont/Font.h>
#include <LibGUI/Widget/Button.h>
#include <LibGUI/Window.h>
namespace LibGUI::Widget
{
BAN::ErrorOr<BAN::RefPtr<Button>> Button::create(BAN::RefPtr<Widget> parent, BAN::StringView text, Rectangle geometry)
{
auto* button_ptr = new Button(parent, geometry);
if (button_ptr == nullptr)
return BAN::Error::from_errno(ENOMEM);
auto button = BAN::RefPtr<Button>::adopt(button_ptr);
TRY(button->initialize(color_invisible));
TRY(button->m_text.append(text));
return button;
}
BAN::ErrorOr<void> Button::set_text(BAN::StringView text)
{
m_text.clear();
TRY(m_text.append(text));
if (is_shown())
show();
return {};
}
void Button::update_impl()
{
const bool hover_color = is_hovered() && !is_child_hovered();
if (hover_color != m_hover_state)
show();
}
void Button::show_impl()
{
m_hover_state = is_hovered() && !is_child_hovered();
const auto& font = default_font();
const int32_t text_h = font.height();
const int32_t text_w = font.width() * m_text.size();
const int32_t off_x = (static_cast<int32_t>(width()) - text_w) / 2;
const int32_t off_y = (static_cast<int32_t>(height()) - text_h) / 2;
m_texture.fill(m_hover_state ? m_style.color_hovered : m_style.color_normal);
m_texture.draw_text(m_text, font, off_x, off_y, m_style.color_text);
RoundedWidget::style() = m_style;
RoundedWidget::show_impl();
}
bool Button::on_mouse_button_impl(LibGUI::EventPacket::MouseButtonEvent::event_t event)
{
if (event.pressed && event.button == LibInput::MouseButton::Left && m_click_callback)
m_click_callback();
return true;
}
}

View File

@@ -0,0 +1,53 @@
#include <LibGUI/Widget/Grid.h>
namespace LibGUI::Widget
{
BAN::ErrorOr<BAN::RefPtr<Grid>> Grid::create(BAN::RefPtr<Widget> parent, uint32_t cols, uint32_t rows, uint32_t color, Rectangle geometry)
{
auto* grid_ptr = new Grid(parent, geometry, cols, rows);
if (grid_ptr == nullptr)
return BAN::Error::from_errno(ENOMEM);
auto grid = BAN::RefPtr<Grid>::adopt(grid_ptr);
TRY(grid->initialize(color));
return grid;
}
Widget::Rectangle Grid::grid_element_area(const GridElement& element) const
{
const uint32_t this_x = element.col * width() / m_cols;
const uint32_t this_y = element.row * height() / m_rows;
const uint32_t next_x = (element.col + element.col_span) * width() / m_cols;
const uint32_t next_y = (element.row + element.row_span) * height() / m_rows;
return Widget::Rectangle {
.x = static_cast<int32_t>(this_x),
.y = static_cast<int32_t>(this_y),
.w = next_x - this_x,
.h = next_y - this_y,
};
}
BAN::ErrorOr<void> Grid::update_geometry_impl()
{
for (auto& grid_element : m_grid_elements)
TRY(grid_element.widget->set_fixed_geometry(grid_element_area(grid_element)));
return {};
}
BAN::ErrorOr<void> Grid::set_widget_position(BAN::RefPtr<Widget> widget, uint32_t col, uint32_t col_span, uint32_t row, uint32_t row_span)
{
if (col_span == 0 || row_span == 0)
return BAN::Error::from_errno(EINVAL);
if (col + col_span > m_cols)
return BAN::Error::from_errno(EINVAL);
if (row + row_span > m_rows)
return BAN::Error::from_errno(EINVAL);
ASSERT(widget->parent() == this);
TRY(m_grid_elements.emplace_back(widget, col, col_span, row, row_span));
TRY(widget->set_fixed_geometry(grid_element_area(m_grid_elements.back())));
return {};
}
}

View File

@@ -0,0 +1,44 @@
#include <LibFont/Font.h>
#include <LibGUI/Widget/Label.h>
#include <LibGUI/Window.h>
namespace LibGUI::Widget
{
BAN::ErrorOr<BAN::RefPtr<Label>> Label::create(BAN::RefPtr<Widget> parent, BAN::StringView text, Rectangle geometry)
{
auto* label_ptr = new Label(parent, geometry);
if (label_ptr == nullptr)
return BAN::Error::from_errno(ENOMEM);
auto label = BAN::RefPtr<Label>::adopt(label_ptr);
TRY(label->initialize(color_invisible));
TRY(label->m_text.append(text));
return label;
}
BAN::ErrorOr<void> Label::set_text(BAN::StringView text)
{
m_text.clear();
TRY(m_text.append(text));
if (is_shown())
show();
return {};
}
void Label::show_impl()
{
const auto& font = default_font();
const int32_t text_h = font.height();
const int32_t text_w = font.width() * m_text.size();
const int32_t off_x = (static_cast<int32_t>(width()) - text_w) / 2;
const int32_t off_y = (static_cast<int32_t>(height()) - text_h) / 2;
m_texture.fill(m_style.color_normal);
m_texture.draw_text(m_text, font, off_x, off_y, m_style.color_text);
RoundedWidget::style() = m_style;
RoundedWidget::show_impl();
}
}

View File

@@ -0,0 +1,117 @@
#include <LibGUI/Widget/RoundedWidget.h>
namespace LibGUI::Widget
{
bool RoundedWidget::contains(Point point) const
{
if (!Widget::contains(point))
return false;
const auto is_outside_corner =
[this]<uint8_t quadrant>(Point point) -> bool
{
const auto radius = m_style.corner_radius;
const uint32_t base_x = (quadrant % 2) ? m_texture.width() - radius - 1 : 0;
const uint32_t base_y = (quadrant / 2) ? m_texture.height() - radius - 1 : 0;
if (point.x < static_cast<int32_t>(base_x) || point.x > static_cast<int32_t>(base_x + radius))
return false;
if (point.y < static_cast<int32_t>(base_y) || point.y > static_cast<int32_t>(base_y + radius))
return false;
const uint32_t x_off = point.x - base_x;
const uint32_t y_off = point.y - base_y;
const uint32_t dx = ((quadrant % 2) ? x_off : radius - x_off);
const uint32_t dy = ((quadrant / 2) ? y_off : radius - y_off);
const uint32_t distance = dx * dx + dy * dy;
return distance >= radius * radius;
};
if (is_outside_corner.operator()<0>(point))
return false;
if (is_outside_corner.operator()<1>(point))
return false;
if (is_outside_corner.operator()<2>(point))
return false;
if (is_outside_corner.operator()<3>(point))
return false;
return true;
}
void RoundedWidget::show_impl()
{
if (m_style.border_width)
{
m_texture.fill_rect(
0,
0,
m_texture.width(),
m_style.border_width,
m_style.color_border
);
m_texture.fill_rect(
0,
0,
m_style.border_width,
m_texture.height(),
m_style.color_border
);
m_texture.fill_rect(
0,
m_texture.height() - m_style.border_width,
m_texture.width(),
m_style.border_width,
m_style.color_border
);
m_texture.fill_rect(
m_texture.width() - m_style.border_width,
0,
m_style.border_width,
m_texture.height(),
m_style.color_border
);
}
if (m_style.corner_radius)
{
const auto draw_corner =
[this]<uint8_t quadrant>()
{
const auto radius = m_style.corner_radius;
const uint32_t base_x = (quadrant % 2) ? m_texture.width() - radius - 1 : 0;
const uint32_t base_y = (quadrant / 2) ? m_texture.height() - radius - 1 : 0;
const uint32_t distance_max = radius * radius;
const uint32_t distance_min = (radius - m_style.border_width) * (radius - m_style.border_width);
for (uint32_t y_off = 0; y_off <= radius; y_off++)
{
for (uint32_t x_off = 0; x_off <= radius; x_off++)
{
const uint32_t dx = ((quadrant % 2) ? x_off : radius - x_off);
const uint32_t dy = ((quadrant / 2) ? y_off : radius - y_off);
const uint32_t distance = dx * dx + dy * dy;
if (base_x + x_off >= m_texture.width())
continue;
if (base_y + y_off >= m_texture.height())
continue;
if (distance > distance_max)
m_texture.set_pixel(base_x + x_off, base_y + y_off, color_invisible);
else if (distance >= distance_min)
m_texture.set_pixel(base_x + x_off, base_y + y_off, m_style.color_border);
}
}
};
draw_corner.operator()<0>();
draw_corner.operator()<1>();
draw_corner.operator()<2>();
draw_corner.operator()<3>();
}
}
}

View File

@@ -0,0 +1,96 @@
#include <LibFont/Font.h>
#include <LibGUI/Widget/TextArea.h>
#include <LibGUI/Window.h>
#include <ctype.h>
namespace LibGUI::Widget
{
BAN::ErrorOr<BAN::RefPtr<TextArea>> TextArea::create(BAN::RefPtr<Widget> parent, BAN::StringView text, Rectangle geometry)
{
auto* text_area_ptr = new TextArea(parent, geometry);
if (text_area_ptr == nullptr)
return BAN::Error::from_errno(ENOMEM);
auto text_area = BAN::RefPtr<TextArea>::adopt(text_area_ptr);
TRY(text_area->initialize(color_invisible));
TRY(text_area->set_text(text));
return text_area;
}
BAN::ErrorOr<void> TextArea::set_text(BAN::StringView text)
{
m_text.clear();
TRY(m_text.append(text));
TRY(wrap_text());
if (is_shown())
show();
return {};
}
uint32_t TextArea::get_required_height() const
{
auto& font = default_font();
return m_wrapped_text.size() * font.height();
}
BAN::ErrorOr<void> TextArea::wrap_text()
{
m_wrapped_text.clear();
if (width() == 0)
return {};
auto& font = default_font();
const uint32_t total_columns = width() / font.width();
ASSERT(total_columns != 0);
TRY(m_wrapped_text.emplace_back());
for (size_t i = 0; i < m_text.size(); i++)
{
if (m_text[i] == '\n')
TRY(m_wrapped_text.emplace_back());
else if (isspace(m_text[i]) && m_wrapped_text.back().size() == 0)
;
else
{
TRY(m_wrapped_text.back().push_back(m_text[i]));
if (i + 1 < m_text.size() && isspace(m_text[i]) && !isspace(m_text[i + 1]))
{
size_t word_len = 0;
for (size_t j = i + 1; j < m_text.size() && !isspace(m_text[j]); j++)
word_len++;
if (word_len <= total_columns && m_wrapped_text.back().size() + word_len > total_columns)
TRY(m_wrapped_text.emplace_back());
}
if (m_wrapped_text.back().size() >= total_columns)
TRY(m_wrapped_text.emplace_back());
}
}
return {};
}
BAN::ErrorOr<void> TextArea::update_geometry_impl()
{
TRY(wrap_text());
return Widget::update_geometry_impl();
}
void TextArea::show_impl()
{
const auto& font = default_font();
m_texture.fill(m_style.color_normal);
for (int32_t row = 0; row < static_cast<int32_t>(m_wrapped_text.size()); row++)
m_texture.draw_text(m_wrapped_text[row].sv(), font, 0, row * font.height(), m_style.color_text);
RoundedWidget::style() = m_style;
RoundedWidget::show_impl();
}
}

View File

@@ -0,0 +1,241 @@
#include <LibGUI/Widget/Widget.h>
#include <LibFont/Font.h>
namespace LibGUI::Widget
{
static BAN::Optional<LibFont::Font> s_default_font;
BAN::ErrorOr<void> Widget::set_default_font(BAN::StringView path)
{
s_default_font = TRY(LibFont::Font::load(path));
return {};
}
const LibFont::Font& Widget::default_font()
{
if (!s_default_font.has_value())
MUST(set_default_font("/usr/share/fonts/lat0-16.psfu"_sv));
return s_default_font.value();
}
BAN::ErrorOr<BAN::RefPtr<Widget>> Widget::create(BAN::RefPtr<Widget> parent, uint32_t color, Rectangle area)
{
auto* widget_ptr = new Widget(parent, area);
if (widget_ptr == nullptr)
return BAN::Error::from_errno(ENOMEM);
auto widget = BAN::RefPtr<Widget>::adopt(widget_ptr);
TRY(widget->initialize(color));
return widget;
}
BAN::ErrorOr<void> Widget::initialize(uint32_t color)
{
m_texture = TRY(Texture::create(width(), height(), color));
if (m_parent)
TRY(m_parent->m_children.push_back(this));
return {};
}
bool Widget::is_child_hovered() const
{
for (auto child : m_children)
if (child->m_hovered || child->is_child_hovered())
return true;
return false;
}
BAN::ErrorOr<void> Widget::set_fixed_geometry(Rectangle area)
{
TRY(m_texture.resize(area.w, area.h));
m_fixed_area = area;
m_relative_area.clear();
TRY(update_geometry_impl());
if (is_shown())
show();
return {};
}
BAN::ErrorOr<void> Widget::set_relative_geometry(FloatRectangle area)
{
if (area.w < 0.0f || area.h < 0.0f)
return BAN::Error::from_errno(EINVAL);
ASSERT(m_parent);
TRY(set_fixed_geometry({
.x = static_cast<int32_t>(area.x * m_parent->width()),
.y = static_cast<int32_t>(area.y * m_parent->height()),
.w = static_cast<uint32_t>(area.w * m_parent->width()),
.h = static_cast<uint32_t>(area.h * m_parent->height()),
}));
m_relative_area = area;
return {};
}
BAN::ErrorOr<void> Widget::update_geometry_impl()
{
for (auto child : m_children)
{
if (!child->m_relative_area.has_value())
continue;
TRY(child->set_relative_geometry(child->m_relative_area.value()));
}
return {};
}
void Widget::show()
{
m_shown = true;
show_impl();
m_changed = true;
for (auto child : m_children)
if (child->is_shown())
child->show();
}
void Widget::hide()
{
if (!is_shown())
return;
m_shown = false;
auto root = m_parent;
while (root && root->m_parent)
root = root->m_parent;
if (root)
root->show();
}
void Widget::before_mouse_move()
{
if (!is_shown())
return;
m_old_hovered = m_hovered;
m_hovered = false;
for (auto child : m_children)
child->before_mouse_move();
}
void Widget::after_mouse_move()
{
if (!is_shown())
return;
if (m_old_hovered != m_hovered)
on_hover_change_impl(m_hovered);
for (auto child : m_children)
child->after_mouse_move();
}
bool Widget::on_mouse_move(LibGUI::EventPacket::MouseMoveEvent::event_t event)
{
if (!Rectangle { 0, 0, width(), height() }.contains({ event.x, event.y }))
return false;
if (!is_shown())
return false;
m_hovered = contains({ event.x, event.y });
for (auto child : m_children)
{
auto rel_event = event;
rel_event.x -= child->m_fixed_area.x;
rel_event.y -= child->m_fixed_area.y;
if (child->on_mouse_move(rel_event))
return true;
}
if (!m_hovered)
return false;
return on_mouse_move_impl(event);
}
bool Widget::on_mouse_button(LibGUI::EventPacket::MouseButtonEvent::event_t event)
{
if (!Rectangle { 0, 0, width(), height() }.contains({ event.x, event.y }))
return false;
if (!is_shown())
return false;
for (auto child : m_children)
{
auto rel_event = event;
rel_event.x -= child->m_fixed_area.x;
rel_event.y -= child->m_fixed_area.y;
if (child->on_mouse_button(rel_event))
return true;
}
if (!contains({ event.x, event.y }))
return false;
return on_mouse_button_impl(event);
}
Widget::Rectangle Widget::render(Texture& output, Point parent_position, Rectangle out_area)
{
if (!is_shown())
return {};
update_impl();
Rectangle invalidated {};
if (m_changed)
{
auto where_i_would_be = Rectangle {
.x = parent_position.x + m_fixed_area.x,
.y = parent_position.y + m_fixed_area.y,
.w = m_fixed_area.w,
.h = m_fixed_area.h,
};
auto where_to_draw = out_area.overlap(where_i_would_be);
if (where_to_draw.w && where_to_draw.h)
{
output.copy_texture(
m_texture,
where_to_draw.x,
where_to_draw.y,
where_to_draw.x - where_i_would_be.x,
where_to_draw.y - where_i_would_be.y,
where_to_draw.w,
where_to_draw.h
);
}
invalidated = where_to_draw;
m_changed = false;
}
out_area = out_area.overlap({
.x = parent_position.x + m_fixed_area.x,
.y = parent_position.y + m_fixed_area.y,
.w = m_fixed_area.w,
.h = m_fixed_area.h,
});
parent_position = {
.x = parent_position.x + m_fixed_area.x,
.y = parent_position.y + m_fixed_area.y,
};
for (auto child : m_children)
{
invalidated = invalidated.bounding_box(
child->render(output, parent_position, out_area)
);
}
return invalidated;
}
}