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/Painter.h>
28#include <LibHTML/DOM/Document.h>
29#include <LibHTML/DOM/HTMLBodyElement.h>
30#include <LibHTML/Frame.h>
31#include <LibHTML/Layout/LayoutBlock.h>
32#include <LibHTML/Layout/LayoutBox.h>
33
34//#define DRAW_BOXES_AROUND_LAYOUT_NODES
35//#define DRAW_BOXES_AROUND_HOVERED_NODES
36
37void LayoutBox::paint_border(RenderingContext& context, Edge edge, const Gfx::FloatRect& rect, CSS::PropertyID style_property_id, CSS::PropertyID color_property_id, CSS::PropertyID width_property_id)
38{
39 auto border_width = style().property(width_property_id);
40 if (!border_width.has_value())
41 return;
42
43 auto border_style = style().property(style_property_id);
44 float width = border_width.value()->to_length().to_px();
45
46 int int_width = max((int)width, 1);
47
48 Color color;
49 auto border_color = style().property(color_property_id);
50 if (border_color.has_value()) {
51 color = border_color.value()->to_color(document());
52 } else {
53 // FIXME: This is basically CSS "currentColor" which should be handled elsewhere
54 // in a much more reusable way.
55 auto current_color = style().property(CSS::PropertyID::Color);
56 if (current_color.has_value())
57 color = current_color.value()->to_color(document());
58 else
59 color = Color::Black;
60 }
61
62 auto first_point_for_edge = [](Edge edge, const Gfx::FloatRect& rect) {
63 switch (edge) {
64 case Edge::Top:
65 return rect.top_left();
66 case Edge::Right:
67 return rect.top_right();
68 case Edge::Bottom:
69 return rect.bottom_left();
70 case Edge::Left:
71 default:
72 return rect.top_left();
73 }
74 };
75
76 auto second_point_for_edge = [](Edge edge, const Gfx::FloatRect& rect) {
77 switch (edge) {
78 case Edge::Top:
79 return rect.top_right();
80 case Edge::Right:
81 return rect.bottom_right();
82 case Edge::Bottom:
83 return rect.bottom_right();
84 case Edge::Left:
85 default:
86 return rect.bottom_left();
87 }
88 };
89
90 auto p1 = first_point_for_edge(edge, rect);
91 auto p2 = second_point_for_edge(edge, rect);
92
93 if (border_style.has_value() && border_style.value()->to_string() == "inset") {
94 auto top_left_color = Color::from_rgb(0x5a5a5a);
95 auto bottom_right_color = Color::from_rgb(0x888888);
96 color = (edge == Edge::Left || edge == Edge::Top) ? top_left_color : bottom_right_color;
97 } else if (border_style.has_value() && border_style.value()->to_string() == "outset") {
98 auto top_left_color = Color::from_rgb(0x888888);
99 auto bottom_right_color = Color::from_rgb(0x5a5a5a);
100 color = (edge == Edge::Left || edge == Edge::Top) ? top_left_color : bottom_right_color;
101 }
102
103 bool dotted = border_style.has_value() && border_style.value()->to_string() == "dotted";
104
105 auto draw_line = [&](auto& p1, auto& p2) {
106 context.painter().draw_line({ (int)p1.x(), (int)p1.y() }, { (int)p2.x(), (int)p2.y() }, color, 1, dotted);
107 };
108
109 auto width_for = [&](CSS::PropertyID property_id) -> float {
110 auto width = style().property(property_id);
111 if (!width.has_value())
112 return 0;
113 return width.value()->to_length().to_px();
114 };
115
116 float p1_step = 0;
117 float p2_step = 0;
118
119 switch (edge) {
120 case Edge::Top:
121 p1_step = width_for(CSS::PropertyID::BorderLeftWidth) / (float)int_width;
122 p2_step = width_for(CSS::PropertyID::BorderRightWidth) / (float)int_width;
123 for (int i = 0; i < int_width; ++i) {
124 draw_line(p1, p2);
125 p1.move_by(p1_step, 1);
126 p2.move_by(-p2_step, 1);
127 }
128 break;
129 case Edge::Right:
130 p1_step = width_for(CSS::PropertyID::BorderTopWidth) / (float)int_width;
131 p2_step = width_for(CSS::PropertyID::BorderBottomWidth) / (float)int_width;
132 for (int i = int_width - 1; i >= 0; --i) {
133 draw_line(p1, p2);
134 p1.move_by(-1, p1_step);
135 p2.move_by(-1, -p2_step);
136 }
137 break;
138 case Edge::Bottom:
139 p1_step = width_for(CSS::PropertyID::BorderLeftWidth) / (float)int_width;
140 p2_step = width_for(CSS::PropertyID::BorderRightWidth) / (float)int_width;
141 for (int i = int_width - 1; i >= 0; --i) {
142 draw_line(p1, p2);
143 p1.move_by(p1_step, -1);
144 p2.move_by(-p2_step, -1);
145 }
146 break;
147 case Edge::Left:
148 p1_step = width_for(CSS::PropertyID::BorderTopWidth) / (float)int_width;
149 p2_step = width_for(CSS::PropertyID::BorderBottomWidth) / (float)int_width;
150 for (int i = 0; i < int_width; ++i) {
151 draw_line(p1, p2);
152 p1.move_by(1, p1_step);
153 p2.move_by(1, -p2_step);
154 }
155 break;
156 }
157}
158
159void LayoutBox::render(RenderingContext& context)
160{
161 if (!is_visible())
162 return;
163
164#ifdef DRAW_BOXES_AROUND_LAYOUT_NODES
165 context.painter().draw_rect(m_rect, Color::Blue);
166#endif
167#ifdef DRAW_BOXES_AROUND_HOVERED_NODES
168 if (!is_anonymous() && node() == document().hovered_node())
169 context.painter().draw_rect(m_rect, Color::Red);
170#endif
171
172 if (node() && document().inspected_node() == node())
173 context.painter().draw_rect(enclosing_int_rect(m_rect), Color::Magenta);
174
175 Gfx::FloatRect padded_rect;
176 padded_rect.set_x(x() - box_model().padding().left.to_px());
177 padded_rect.set_width(width() + box_model().padding().left.to_px() + box_model().padding().right.to_px());
178 padded_rect.set_y(y() - box_model().padding().top.to_px());
179 padded_rect.set_height(height() + box_model().padding().top.to_px() + box_model().padding().bottom.to_px());
180
181 if (!is_body()) {
182 auto bgcolor = style().property(CSS::PropertyID::BackgroundColor);
183 if (bgcolor.has_value() && bgcolor.value()->is_color()) {
184 context.painter().fill_rect(enclosing_int_rect(padded_rect), bgcolor.value()->to_color(document()));
185 }
186
187 auto bgimage = style().property(CSS::PropertyID::BackgroundImage);
188 if (bgimage.has_value() && bgimage.value()->is_image()) {
189 auto& image_value = static_cast<const ImageStyleValue&>(*bgimage.value());
190 if (image_value.bitmap()) {
191 context.painter().draw_tiled_bitmap(enclosing_int_rect(padded_rect), *image_value.bitmap());
192 }
193 }
194 }
195
196 Gfx::FloatRect bordered_rect;
197 bordered_rect.set_x(padded_rect.x() - box_model().border().left.to_px());
198 bordered_rect.set_width(padded_rect.width() + box_model().border().left.to_px() + box_model().border().right.to_px());
199 bordered_rect.set_y(padded_rect.y() - box_model().border().top.to_px());
200 bordered_rect.set_height(padded_rect.height() + box_model().border().top.to_px() + box_model().border().bottom.to_px());
201
202 paint_border(context, Edge::Left, bordered_rect, CSS::PropertyID::BorderLeftStyle, CSS::PropertyID::BorderLeftColor, CSS::PropertyID::BorderLeftWidth);
203 paint_border(context, Edge::Right, bordered_rect, CSS::PropertyID::BorderRightStyle, CSS::PropertyID::BorderRightColor, CSS::PropertyID::BorderRightWidth);
204 paint_border(context, Edge::Top, bordered_rect, CSS::PropertyID::BorderTopStyle, CSS::PropertyID::BorderTopColor, CSS::PropertyID::BorderTopWidth);
205 paint_border(context, Edge::Bottom, bordered_rect, CSS::PropertyID::BorderBottomStyle, CSS::PropertyID::BorderBottomColor, CSS::PropertyID::BorderBottomWidth);
206
207 LayoutNodeWithStyleAndBoxModelMetrics::render(context);
208}
209
210HitTestResult LayoutBox::hit_test(const Gfx::Point& position) const
211{
212 // FIXME: It would be nice if we could confidently skip over hit testing
213 // parts of the layout tree, but currently we can't just check
214 // m_rect.contains() since inline text rects can't be trusted..
215 HitTestResult result { m_rect.contains(position.x(), position.y()) ? this : nullptr };
216 for_each_child([&](auto& child) {
217 auto child_result = child.hit_test(position);
218 if (child_result.layout_node)
219 result = child_result;
220 });
221 return result;
222}
223
224void LayoutBox::set_needs_display()
225{
226 auto* frame = document().frame();
227 ASSERT(frame);
228
229 if (!is_inline()) {
230 const_cast<Frame*>(frame)->set_needs_display(enclosing_int_rect(rect()));
231 return;
232 }
233
234 LayoutNode::set_needs_display();
235}
236
237bool LayoutBox::is_body() const
238{
239 return node() && node() == document().body();
240}