Serenity Operating System
at master 473 lines 18 kB view raw
1/* 2 * Copyright (c) 2021-2022, the SerenityOS developers. 3 * Copyright (c) 2022-2023, Sam Atkins <atkinssj@serenityos.org> 4 * 5 * SPDX-License-Identifier: BSD-2-Clause 6 */ 7 8#include "TreeMapWidget.h" 9#include "ProgressWindow.h" 10#include "Tree.h" 11#include <AK/Array.h> 12#include <AK/DeprecatedString.h> 13#include <AK/NumberFormat.h> 14#include <LibGUI/ConnectionToWindowServer.h> 15#include <LibGUI/Painter.h> 16#include <LibGUI/Statusbar.h> 17#include <LibGfx/Font/Font.h> 18#include <WindowServer/WindowManager.h> 19 20REGISTER_WIDGET(SpaceAnalyzer, TreeMapWidget) 21 22namespace SpaceAnalyzer { 23 24static constexpr Array colors = { 25 Color(253, 231, 37), 26 Color(148, 216, 64), 27 Color(60, 188, 117), 28 Color(31, 150, 139), 29 Color(45, 112, 142), 30 Color(63, 71, 136), 31 Color(85, 121, 104), 32}; 33 34static float get_normalized_aspect_ratio(float a, float b) 35{ 36 if (a < b) { 37 return a / b; 38 } else { 39 return b / a; 40 } 41} 42 43static bool node_is_leaf(TreeNode const& node) 44{ 45 return node.num_children() == 0; 46} 47 48bool TreeMapWidget::rect_can_contain_label(Gfx::IntRect const& rect) const 49{ 50 return rect.height() >= font().presentation_size() && rect.width() > 20; 51} 52 53void TreeMapWidget::paint_cell_frame(GUI::Painter& painter, TreeNode const& node, Gfx::IntRect const& cell_rect, Gfx::IntRect const& inner_rect, int depth, HasLabel has_label) const 54{ 55 if (cell_rect.width() <= 2 || cell_rect.height() <= 2) { 56 painter.fill_rect(cell_rect, Color::Black); 57 return; 58 } 59 Gfx::IntRect remainder = cell_rect; 60 61 Color color = colors[depth % (sizeof(colors) / sizeof(colors[0]))]; 62 if (m_selected_node_cache == &node) { 63 color = color.darkened(0.8f); 64 } 65 66 // Draw borders. 67 painter.fill_rect(remainder.take_from_right(1), Color::Black); 68 painter.fill_rect(remainder.take_from_bottom(1), Color::Black); 69 // Draw highlights. 70 painter.fill_rect(remainder.take_from_right(1), color.darkened()); 71 painter.fill_rect(remainder.take_from_bottom(1), color.darkened()); 72 painter.fill_rect(remainder.take_from_top(1), color.lightened()); 73 painter.fill_rect(remainder.take_from_left(1), color.lightened()); 74 75 // Paint the background. 76 if (inner_rect.is_empty()) { 77 painter.fill_rect(remainder, color); 78 } else { 79 // Draw black edges above and to the left of the inner_rect. 80 Gfx::IntRect border_rect = inner_rect.inflated(2, 2); 81 Gfx::IntRect hammer_rect = border_rect; 82 hammer_rect.set_width(hammer_rect.width() - 1); 83 hammer_rect.set_height(hammer_rect.height() - 1); 84 painter.fill_rect(border_rect.take_from_top(1), Color::Black); 85 painter.fill_rect(border_rect.take_from_left(1), Color::Black); 86 for (auto& shard : remainder.shatter(hammer_rect)) { 87 painter.fill_rect(shard, color); 88 } 89 } 90 91 // Paint text. 92 if (has_label == HasLabel::Yes) { 93 Gfx::IntRect text_rect = remainder; 94 text_rect.shrink(4, 4); 95 painter.clear_clip_rect(); 96 painter.add_clip_rect(text_rect); 97 if (node_is_leaf(node)) { 98 painter.draw_text(text_rect, node.name(), font(), Gfx::TextAlignment::TopLeft, Color::Black); 99 text_rect.take_from_top(font().presentation_size() + 1); 100 painter.draw_text(text_rect, human_readable_size(node.area()), font(), Gfx::TextAlignment::TopLeft, Color::Black); 101 } else { 102 painter.draw_text(text_rect, DeprecatedString::formatted("{} - {}", node.name(), human_readable_size(node.area())), font(), Gfx::TextAlignment::TopLeft, Color::Black); 103 } 104 painter.clear_clip_rect(); 105 } 106} 107 108template<typename Function> 109void TreeMapWidget::lay_out_children(TreeNode const& node, Gfx::IntRect const& rect, int depth, Function callback) 110{ 111 if (node.num_children() == 0) { 112 return; 113 } 114 115 // Check if the children are sorted yet, if not do that now. 116 for (size_t k = 0; k < node.num_children() - 1; k++) { 117 if (node.child_at(k).area() < node.child_at(k + 1).area()) { 118 node.sort_children_by_area(); 119 break; 120 } 121 } 122 123 i64 total_area = node.area(); 124 Gfx::IntRect canvas = rect; 125 bool remaining_nodes_are_too_small = false; 126 for (size_t i = 0; !remaining_nodes_are_too_small && i < node.num_children(); i++) { 127 const i64 i_node_area = node.child_at(i).area(); 128 if (i_node_area == 0) 129 break; 130 131 const size_t long_side_size = max(canvas.width(), canvas.height()); 132 const size_t short_side_size = min(canvas.width(), canvas.height()); 133 134 size_t row_or_column_size = long_side_size * i_node_area / total_area; 135 i64 node_area_sum = i_node_area; 136 size_t k = i + 1; 137 138 // Try to add nodes to this row or column so long as the worst aspect ratio of 139 // the new set of nodes is better than the worst aspect ratio of the current set. 140 { 141 float best_worst_aspect_ratio_so_far = get_normalized_aspect_ratio(row_or_column_size, short_side_size); 142 for (; k < node.num_children(); k++) { 143 // Do a preliminary calculation of the worst aspect ratio of the nodes at index i and k 144 // if that aspect ratio is better than the 'best_worst_aspect_ratio_so_far' we keep it, 145 // otherwise it is discarded. 146 i64 k_node_area = node.child_at(k).area(); 147 if (k_node_area == 0) { 148 break; 149 } 150 i64 new_node_area_sum = node_area_sum + k_node_area; 151 size_t new_row_or_column_size = long_side_size * new_node_area_sum / total_area; 152 size_t i_node_size = short_side_size * i_node_area / new_node_area_sum; 153 size_t k_node_size = short_side_size * k_node_area / new_node_area_sum; 154 float i_node_aspect_ratio = get_normalized_aspect_ratio(new_row_or_column_size, i_node_size); 155 float k_node_aspect_ratio = get_normalized_aspect_ratio(new_row_or_column_size, k_node_size); 156 float new_worst_aspect_ratio = min(i_node_aspect_ratio, k_node_aspect_ratio); 157 if (new_worst_aspect_ratio < best_worst_aspect_ratio_so_far) { 158 break; 159 } 160 best_worst_aspect_ratio_so_far = new_worst_aspect_ratio; 161 node_area_sum = new_node_area_sum; 162 row_or_column_size = new_row_or_column_size; 163 } 164 } 165 166 // Paint the elements from 'i' up to and including 'k-1'. 167 { 168 const size_t fixed_side_size = row_or_column_size; 169 i64 placement_area = node_area_sum; 170 size_t main_dim = short_side_size; 171 172 // Lay out nodes in a row or column. 173 Orientation orientation = canvas.width() > canvas.height() ? Orientation::Horizontal : Orientation::Vertical; 174 Gfx::IntRect layout_rect = canvas; 175 layout_rect.set_primary_size_for_orientation(orientation, fixed_side_size); 176 for (size_t q = i; q < k; q++) { 177 auto& child = node.child_at(q); 178 size_t node_size = main_dim * child.area() / placement_area; 179 Gfx::IntRect cell_rect = layout_rect; 180 cell_rect.set_secondary_size_for_orientation(orientation, node_size); 181 Gfx::IntRect inner_rect; 182 HasLabel has_label = HasLabel::No; 183 if (child.num_children() != 0 && rect.height() >= 8 && rect.width() >= 8) { 184 inner_rect = cell_rect; 185 inner_rect.shrink(4, 4); // border and shading 186 if (rect_can_contain_label(inner_rect)) { 187 int const margin = 5; 188 has_label = HasLabel::Yes; 189 inner_rect.set_y(inner_rect.y() + font().presentation_size() + margin); 190 inner_rect.set_height(inner_rect.height() - (font().presentation_size() + margin * 2)); 191 inner_rect.set_x(inner_rect.x() + margin); 192 inner_rect.set_width(inner_rect.width() - margin * 2); 193 } 194 } else if (rect_can_contain_label(cell_rect)) { 195 has_label = HasLabel::Yes; 196 } 197 callback(child, q, cell_rect, inner_rect, depth, has_label, IsRemainder::No); 198 if (cell_rect.width() * cell_rect.height() < 16) { 199 remaining_nodes_are_too_small = true; 200 } else if (!inner_rect.is_empty()) { 201 lay_out_children(child, inner_rect, depth + 1, callback); 202 } 203 layout_rect.set_secondary_offset_for_orientation(orientation, layout_rect.secondary_offset_for_orientation(orientation) + node_size); 204 main_dim -= node_size; 205 placement_area -= child.area(); 206 } 207 canvas.set_primary_offset_for_orientation(orientation, canvas.primary_offset_for_orientation(orientation) + fixed_side_size); 208 canvas.set_primary_size_for_orientation(orientation, canvas.primary_size_for_orientation(orientation) - fixed_side_size); 209 } 210 211 // Consume nodes that were added to this row or column. 212 i = k - 1; 213 total_area -= node_area_sum; 214 } 215 216 // If not the entire canvas was filled with nodes, fill the remaining area with a dither pattern. 217 if (!canvas.is_empty()) { 218 callback(node, 0, canvas, Gfx::IntRect(), depth, HasLabel::No, IsRemainder::Yes); 219 } 220} 221 222TreeNode const* TreeMapWidget::path_node(size_t n) const 223{ 224 if (!m_tree.ptr()) 225 return nullptr; 226 TreeNode const* iter = &m_tree->root(); 227 size_t path_index = 0; 228 while (iter && path_index < m_path_segments.size() && path_index < n) { 229 auto child_name = m_path_segments[path_index]; 230 auto maybe_child = iter->child_with_name(child_name); 231 if (!maybe_child.has_value()) 232 return nullptr; 233 iter = &maybe_child.release_value(); 234 path_index++; 235 } 236 return iter; 237} 238 239void TreeMapWidget::paint_event(GUI::PaintEvent& event) 240{ 241 GUI::Frame::paint_event(event); 242 GUI::Painter painter(*this); 243 244 m_selected_node_cache = path_node(m_path_segments.size()); 245 246 TreeNode const* node = path_node(m_viewpoint); 247 if (!node) { 248 painter.fill_rect(frame_inner_rect(), Color::MidGray); 249 } else if (node_is_leaf(*node)) { 250 paint_cell_frame(painter, *node, frame_inner_rect(), Gfx::IntRect(), m_viewpoint - 1, HasLabel::Yes); 251 } else { 252 lay_out_children(*node, frame_inner_rect(), m_viewpoint, [&](TreeNode const& node, int, Gfx::IntRect const& rect, Gfx::IntRect const& inner_rect, int depth, HasLabel has_label, IsRemainder remainder) { 253 if (remainder == IsRemainder::No) { 254 paint_cell_frame(painter, node, rect, inner_rect, depth, has_label); 255 } else { 256 Color color = colors[depth % (sizeof(colors) / sizeof(colors[0]))]; 257 Gfx::IntRect dither_rect = rect; 258 painter.fill_rect(dither_rect.take_from_right(1), Color::Black); 259 painter.fill_rect(dither_rect.take_from_bottom(1), Color::Black); 260 painter.fill_rect_with_dither_pattern(dither_rect, color, Color::Black); 261 } 262 }); 263 } 264} 265 266Vector<DeprecatedString> TreeMapWidget::path_to_position(Gfx::IntPoint position) 267{ 268 TreeNode const* node = path_node(m_viewpoint); 269 if (!node) { 270 return {}; 271 } 272 Vector<DeprecatedString> path; 273 lay_out_children(*node, frame_inner_rect(), m_viewpoint, [&](TreeNode const& node, int, Gfx::IntRect const& rect, Gfx::IntRect const&, int, HasLabel, IsRemainder is_remainder) { 274 if (is_remainder == IsRemainder::No && rect.contains(position)) { 275 path.append(node.name()); 276 } 277 }); 278 return path; 279} 280 281void TreeMapWidget::mousemove_event(GUI::MouseEvent& event) 282{ 283 auto* node = path_node(m_viewpoint); 284 if (!node) { 285 set_tooltip({}); 286 return; 287 } 288 289 auto* hovered_node = node; 290 lay_out_children(*node, frame_inner_rect(), m_viewpoint, [&](TreeNode const&, int index, Gfx::IntRect const& rect, Gfx::IntRect const&, int, HasLabel, IsRemainder is_remainder) { 291 if (is_remainder == IsRemainder::No && rect.contains(event.position())) { 292 hovered_node = &hovered_node->child_at(index); 293 } 294 }); 295 296 set_tooltip(DeprecatedString::formatted("{}\n{}", hovered_node->name(), human_readable_size(hovered_node->area()))); 297} 298 299void TreeMapWidget::mousedown_event(GUI::MouseEvent& event) 300{ 301 TreeNode const* node = path_node(m_viewpoint); 302 if (node && !node_is_leaf(*node)) { 303 auto path = path_to_position(event.position()); 304 if (!path.is_empty()) { 305 m_path_segments.shrink(m_viewpoint); 306 m_path_segments.extend(path); 307 update(); 308 } 309 } 310} 311 312void TreeMapWidget::doubleclick_event(GUI::MouseEvent& event) 313{ 314 if (event.button() != GUI::MouseButton::Primary) 315 return; 316 TreeNode const* node = path_node(m_viewpoint); 317 if (node && !node_is_leaf(*node)) { 318 auto path = path_to_position(event.position()); 319 m_path_segments.shrink(m_viewpoint); 320 m_path_segments.extend(path); 321 m_viewpoint = m_path_segments.size(); 322 if (on_path_change) { 323 on_path_change(); 324 } 325 update(); 326 } 327} 328 329void TreeMapWidget::keydown_event(GUI::KeyEvent& event) 330{ 331 if (event.key() == KeyCode::Key_Left) 332 set_viewpoint(m_viewpoint == 0 ? m_path_segments.size() : m_viewpoint - 1); 333 else if (event.key() == KeyCode::Key_Right) 334 set_viewpoint(m_viewpoint == m_path_segments.size() ? 0 : m_viewpoint + 1); 335 else 336 event.ignore(); 337} 338 339void TreeMapWidget::mousewheel_event(GUI::MouseEvent& event) 340{ 341 int delta = event.wheel_raw_delta_y(); 342 if (delta > 0) { 343 size_t step_back = delta; 344 if (step_back > m_viewpoint) 345 step_back = m_viewpoint; 346 set_viewpoint(m_viewpoint - step_back); 347 } else { 348 size_t step_up = -delta; 349 set_viewpoint(m_viewpoint + step_up); 350 } 351} 352 353void TreeMapWidget::context_menu_event(GUI::ContextMenuEvent& context_menu_event) 354{ 355 if (on_context_menu_request) 356 on_context_menu_request(context_menu_event); 357} 358 359void TreeMapWidget::recalculate_path_for_new_tree() 360{ 361 TreeNode const* current = &m_tree->root(); 362 size_t new_path_length = 0; 363 for (auto& segment : m_path_segments) { 364 auto maybe_child = current->child_with_name(segment); 365 if (!maybe_child.has_value()) 366 break; 367 new_path_length++; 368 current = &maybe_child.release_value(); 369 } 370 m_path_segments.shrink(new_path_length); 371 if (new_path_length < m_viewpoint) 372 m_viewpoint = new_path_length - 1; 373} 374 375static ErrorOr<void> fill_mounts(Vector<MountInfo>& output) 376{ 377 // Output info about currently mounted filesystems. 378 auto file = TRY(Core::File::open("/sys/kernel/df"sv, Core::File::OpenMode::Read)); 379 380 auto content = TRY(file->read_until_eof()); 381 auto json = TRY(JsonValue::from_string(content)); 382 383 TRY(json.as_array().try_for_each([&output](JsonValue const& value) -> ErrorOr<void> { 384 auto& filesystem_object = value.as_object(); 385 MountInfo mount_info; 386 mount_info.mount_point = filesystem_object.get_deprecated_string("mount_point"sv).value_or({}); 387 mount_info.source = filesystem_object.get_deprecated_string("source"sv).value_or("none"); 388 TRY(output.try_append(mount_info)); 389 return {}; 390 })); 391 392 return {}; 393} 394 395ErrorOr<void> TreeMapWidget::analyze(GUI::Statusbar& statusbar) 396{ 397 statusbar.set_text(""); 398 auto progress_window = TRY(ProgressWindow::try_create("Space Analyzer"sv)); 399 progress_window->show(); 400 401 // Build an in-memory tree mirroring the filesystem and for each node 402 // calculate the sum of the file size for all its descendants. 403 auto tree = TRY(Tree::create("")); 404 Vector<MountInfo> mounts; 405 TRY(fill_mounts(mounts)); 406 auto errors = tree->root().populate_filesize_tree(mounts, [&](size_t processed_file_count) { 407 progress_window->update_progress_label(processed_file_count); 408 }); 409 410 progress_window->close(); 411 412 // Display an error summary in the statusbar. 413 if (!errors.is_empty()) { 414 StringBuilder builder; 415 bool first = true; 416 builder.append("Some directories were not analyzed: "sv); 417 for (auto& key : errors.keys()) { 418 if (!first) { 419 builder.append(", "sv); 420 } 421 auto const* error = strerror(key); 422 builder.append({ error, strlen(error) }); 423 builder.append(" ("sv); 424 int value = errors.get(key).value(); 425 builder.append(DeprecatedString::number(value)); 426 if (value == 1) { 427 builder.append(" time"sv); 428 } else { 429 builder.append(" times"sv); 430 } 431 builder.append(')'); 432 first = false; 433 } 434 statusbar.set_text(builder.to_deprecated_string()); 435 } else { 436 statusbar.set_text("No errors"); 437 } 438 439 m_tree = move(tree); 440 recalculate_path_for_new_tree(); 441 442 if (on_path_change) { 443 on_path_change(); 444 } 445 update(); 446 447 return {}; 448} 449 450void TreeMapWidget::set_viewpoint(size_t viewpoint) 451{ 452 if (m_viewpoint == viewpoint) 453 return; 454 if (viewpoint > m_path_segments.size()) 455 viewpoint = m_path_segments.size(); 456 m_viewpoint = viewpoint; 457 if (on_path_change) { 458 on_path_change(); 459 } 460 update(); 461} 462 463size_t TreeMapWidget::path_size() const 464{ 465 return m_path_segments.size() + 1; 466} 467 468size_t TreeMapWidget::viewpoint() const 469{ 470 return m_viewpoint; 471} 472 473}