Serenity Operating System
1/*
2 * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
3 * All rights reserved.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions are met:
7 *
8 * 1. Redistributions of source code must retain the above copyright notice, this
9 * list of conditions and the following disclaimer.
10 *
11 * 2. Redistributions in binary form must reproduce the above copyright notice,
12 * this list of conditions and the following disclaimer in the documentation
13 * and/or other materials provided with the distribution.
14 *
15 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
16 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
18 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
19 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
20 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
21 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
22 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
23 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
24 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
25 */
26
27#include <LibGUI/BoxLayout.h>
28#include <LibGUI/Painter.h>
29#include <LibGUI/TabWidget.h>
30#include <LibGfx/Font.h>
31#include <LibGfx/Palette.h>
32#include <LibGfx/StylePainter.h>
33
34namespace GUI {
35
36TabWidget::TabWidget()
37{
38}
39
40TabWidget::~TabWidget()
41{
42}
43
44void TabWidget::add_widget(const StringView& title, Widget& widget)
45{
46 m_tabs.append({ title, &widget });
47 add_child(widget);
48}
49
50void TabWidget::remove_widget(Widget& widget)
51{
52 m_tabs.remove_first_matching([&widget](auto& entry) { return &widget == entry.widget; });
53 remove_child(widget);
54}
55
56void TabWidget::set_active_widget(Widget* widget)
57{
58 if (widget == m_active_widget)
59 return;
60
61 if (m_active_widget)
62 m_active_widget->set_visible(false);
63 m_active_widget = widget;
64 if (m_active_widget) {
65 m_active_widget->set_relative_rect(child_rect_for_size(size()));
66 m_active_widget->set_visible(true);
67 deferred_invoke([this](auto&) {
68 if (on_change)
69 on_change(*m_active_widget);
70 });
71 }
72
73 update_bar();
74}
75
76void TabWidget::resize_event(ResizeEvent& event)
77{
78 if (!m_active_widget)
79 return;
80 m_active_widget->set_relative_rect(child_rect_for_size(event.size()));
81}
82
83Gfx::Rect TabWidget::child_rect_for_size(const Gfx::Size& size) const
84{
85 Gfx::Rect rect;
86 switch (m_tab_position) {
87 case TabPosition::Top:
88 rect = { { container_padding(), bar_height() + container_padding() }, { size.width() - container_padding() * 2, size.height() - bar_height() - container_padding() * 2 } };
89 break;
90 case TabPosition::Bottom:
91 rect = { { container_padding(), container_padding() }, { size.width() - container_padding() * 2, size.height() - bar_height() - container_padding() * 2 } };
92 break;
93 }
94 if (rect.is_empty())
95 return {};
96 return rect;
97}
98
99void TabWidget::child_event(Core::ChildEvent& event)
100{
101 if (!event.child() || !Core::is<Widget>(*event.child()))
102 return Widget::child_event(event);
103 auto& child = Core::to<Widget>(*event.child());
104 if (event.type() == Event::ChildAdded) {
105 if (!m_active_widget)
106 set_active_widget(&child);
107 else if (m_active_widget != &child)
108 child.set_visible(false);
109 } else if (event.type() == Event::ChildRemoved) {
110 if (m_active_widget == &child) {
111 Widget* new_active_widget = nullptr;
112 for_each_child_widget([&](auto& new_child) {
113 new_active_widget = &new_child;
114 return IterationDecision::Break;
115 });
116 set_active_widget(new_active_widget);
117 }
118 }
119 Widget::child_event(event);
120}
121
122Gfx::Rect TabWidget::bar_rect() const
123{
124 switch (m_tab_position) {
125 case TabPosition::Top:
126 return { 0, 0, width(), bar_height() };
127 case TabPosition::Bottom:
128 return { 0, height() - bar_height(), width(), bar_height() };
129 }
130 ASSERT_NOT_REACHED();
131}
132
133Gfx::Rect TabWidget::container_rect() const
134{
135 switch (m_tab_position) {
136 case TabPosition::Top:
137 return { 0, bar_height(), width(), height() - bar_height() };
138 case TabPosition::Bottom:
139 return { 0, 0, width(), height() - bar_height() };
140 }
141 ASSERT_NOT_REACHED();
142}
143
144void TabWidget::paint_event(PaintEvent& event)
145{
146 Painter painter(*this);
147 painter.add_clip_rect(event.rect());
148
149 auto container_rect = this->container_rect();
150 auto padding_rect = container_rect;
151 for (int i = 0; i < container_padding(); ++i) {
152 painter.draw_rect(padding_rect, palette().button());
153 padding_rect.shrink(2, 2);
154 }
155
156 Gfx::StylePainter::paint_frame(painter, container_rect, palette(), Gfx::FrameShape::Container, Gfx::FrameShadow::Raised, 2);
157
158 for (size_t i = 0; i < m_tabs.size(); ++i) {
159 if (m_tabs[i].widget == m_active_widget)
160 continue;
161 bool hovered = static_cast<int>(i) == m_hovered_tab_index;
162 auto button_rect = this->button_rect(i);
163 Gfx::StylePainter::paint_tab_button(painter, button_rect, palette(), false, hovered, m_tabs[i].widget->is_enabled());
164 painter.draw_text(button_rect.translated(0, 1), m_tabs[i].title, Gfx::TextAlignment::Center, palette().button_text());
165 }
166
167 for (size_t i = 0; i < m_tabs.size(); ++i) {
168 if (m_tabs[i].widget != m_active_widget)
169 continue;
170 bool hovered = static_cast<int>(i) == m_hovered_tab_index;
171 auto button_rect = this->button_rect(i);
172 Gfx::StylePainter::paint_tab_button(painter, button_rect, palette(), true, hovered, m_tabs[i].widget->is_enabled());
173 painter.draw_text(button_rect.translated(0, 1), m_tabs[i].title, Gfx::TextAlignment::Center, palette().button_text());
174 painter.draw_line(button_rect.bottom_left().translated(1, 1), button_rect.bottom_right().translated(-1, 1), palette().button());
175 break;
176 }
177}
178
179Gfx::Rect TabWidget::button_rect(int index) const
180{
181 int x_offset = 2;
182 for (int i = 0; i < index; ++i)
183 x_offset += m_tabs[i].width(font());
184 Gfx::Rect rect { x_offset, 0, m_tabs[index].width(font()), bar_height() };
185 if (m_tabs[index].widget != m_active_widget) {
186 rect.move_by(0, 2);
187 rect.set_height(rect.height() - 2);
188 } else {
189 rect.move_by(-2, 0);
190 rect.set_width(rect.width() + 4);
191 }
192 rect.move_by(bar_rect().location());
193 return rect;
194}
195
196int TabWidget::TabData::width(const Gfx::Font& font) const
197{
198 return 16 + font.width(title);
199}
200
201void TabWidget::mousedown_event(MouseEvent& event)
202{
203 for (size_t i = 0; i < m_tabs.size(); ++i) {
204 auto button_rect = this->button_rect(i);
205 if (!button_rect.contains(event.position()))
206 continue;
207 set_active_widget(m_tabs[i].widget);
208 return;
209 }
210}
211
212void TabWidget::mousemove_event(MouseEvent& event)
213{
214 int hovered_tab = -1;
215 for (size_t i = 0; i < m_tabs.size(); ++i) {
216 auto button_rect = this->button_rect(i);
217 if (!button_rect.contains(event.position()))
218 continue;
219 hovered_tab = i;
220 if (m_tabs[i].widget == m_active_widget)
221 break;
222 }
223 if (hovered_tab == m_hovered_tab_index)
224 return;
225 m_hovered_tab_index = hovered_tab;
226 update_bar();
227}
228
229void TabWidget::leave_event(Core::Event&)
230{
231 if (m_hovered_tab_index != -1) {
232 m_hovered_tab_index = -1;
233 update_bar();
234 }
235}
236
237void TabWidget::update_bar()
238{
239 auto invalidation_rect = bar_rect();
240 invalidation_rect.set_height(invalidation_rect.height() + 1);
241 update(invalidation_rect);
242}
243
244void TabWidget::set_tab_position(TabPosition tab_position)
245{
246 if (m_tab_position == tab_position)
247 return;
248 m_tab_position = tab_position;
249 if (m_active_widget)
250 m_active_widget->set_relative_rect(child_rect_for_size(size()));
251 update();
252}
253
254int TabWidget::active_tab_index() const
255{
256 for (size_t i = 0; i < m_tabs.size(); i++) {
257 if (m_tabs.at(i).widget == m_active_widget)
258 return i;
259 }
260 return -1;
261}
262}