/* * Copyright (c) 2023, Tim Flynn * * SPDX-License-Identifier: BSD-2-Clause */ #include #include #include #include #include #include #include #include #include #include #include #include #include namespace Ladybird { QIcon load_icon_from_uri(StringView uri) { auto resource = MUST(Core::Resource::load_from_uri(uri)); auto path = qstring_from_ak_string(resource->filesystem_path()); return QIcon { path }; } QIcon icon_from_base64_png(StringView favicon_base64_png, int logical_size) { auto decoded = decode_base64(favicon_base64_png); if (decoded.is_error()) return {}; QPixmap pixmap; if (!pixmap.loadFromData(decoded.value().data(), static_cast(decoded.value().size()), "PNG")) return {}; QIcon icon; for (auto device_pixel_ratio : ICON_DEVICE_PIXEL_RATIOS) { auto size = logical_size * device_pixel_ratio; auto scaled_pixmap = pixmap.scaled(size, size, Qt::KeepAspectRatio, Qt::SmoothTransformation); scaled_pixmap.setDevicePixelRatio(device_pixel_ratio); icon.addPixmap(scaled_pixmap); } return icon; } static QPen chrome_icon_pen(QColor const& color, qreal width) { return QPen(color, width, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin); } static void draw_stroked_icon_path(QPainter& painter, QPainterPath const& path, QColor const& color, qreal width) { QPainterPathStroker stroker; stroker.setWidth(width); stroker.setCapStyle(Qt::RoundCap); stroker.setJoinStyle(Qt::RoundJoin); auto stroke = stroker.createStroke(path); stroke.setFillRule(Qt::WindingFill); painter.fillPath(stroke, color); } static void draw_back_icon(QPainter& painter, QColor const& color) { QPainterPath path; path.moveTo(17.2, 10.3); path.lineTo(3.8, 10.3); path.moveTo(9.7, 5.0); path.lineTo(3.8, 10.3); path.lineTo(9.7, 15.6); draw_stroked_icon_path(painter, path, color, 2.0); } static void draw_forward_icon(QPainter& painter, QColor const& color) { QPainterPath path; path.moveTo(2.8, 10.3); path.lineTo(16.2, 10.3); path.moveTo(10.3, 5.0); path.lineTo(16.2, 10.3); path.lineTo(10.3, 15.6); draw_stroked_icon_path(painter, path, color, 2.0); } static void draw_reload_icon(QPainter& painter, QColor const& color) { QPainterPath shape; shape.setFillRule(Qt::WindingFill); shape.arcMoveTo(QRectF(3.0, 2.5, 14.0, 14.0), 58.0); shape.arcTo(QRectF(3.0, 2.5, 14.0, 14.0), 58.0, 272.0); shape.arcTo(QRectF(4.9, 4.4, 10.2, 10.2), 330.0, -272.0); shape.closeSubpath(); QPainterPath arrow_head; arrow_head.moveTo(17.1, 7.05); arrow_head.lineTo(14.65, 1.55); arrow_head.lineTo(11.35, 7.05); arrow_head.closeSubpath(); shape.addPath(arrow_head); painter.fillPath(shape, color); } static void draw_star_icon(QPainter& painter, QColor const& color, bool filled) { QPainterPath path; path.moveTo(10.0, 4.1); path.lineTo(12.5, 9.0); path.lineTo(18.0, 9.5); path.lineTo(13.9, 13.2); path.lineTo(15.1, 18.7); path.lineTo(10.0, 15.8); path.lineTo(4.9, 18.7); path.lineTo(6.1, 13.2); path.lineTo(2.0, 9.5); path.lineTo(7.5, 9.0); path.closeSubpath(); painter.setPen(chrome_icon_pen(color, filled ? 1.2 : 1.65)); if (filled) painter.setBrush(color); else painter.setBrush(Qt::NoBrush); painter.drawPath(path); } static void draw_vertical_tab_bar_icon(QPainter& painter, QColor const& color, bool expanded) { painter.setPen(chrome_icon_pen(color, 1.55)); painter.setBrush(Qt::NoBrush); painter.drawRoundedRect(QRectF(3.8, 3.2, 12.4, 11.6), 1.6, 1.6); painter.drawLine(QPointF(7.8, 3.6), QPointF(7.8, 14.4)); QPainterPath arrow; if (expanded) { arrow.moveTo(13.1, 6.8); arrow.lineTo(10.3, 9.0); arrow.lineTo(13.1, 11.2); } else { arrow.moveTo(10.7, 6.8); arrow.lineTo(13.5, 9.0); arrow.lineTo(10.7, 11.2); } draw_stroked_icon_path(painter, arrow, color, 1.85); } static void draw_globe_icon(QPainter& painter, QColor const& color) { auto outline_rect = QRectF(3.5, 3.5, 13, 13); QRectF meridian_rect(7.5, outline_rect.top() + 0.25, 5.0, outline_rect.height() - 0.5); QPainterPath path; path.addEllipse(outline_rect); path.moveTo(QPointF(outline_rect.left() + 1.3, 10)); path.lineTo(QPointF(outline_rect.right() - 1.3, 10)); path.arcMoveTo(meridian_rect, 90); path.arcTo(meridian_rect, 90, 180); path.arcMoveTo(meridian_rect, 270); path.arcTo(meridian_rect, 270, 180); draw_stroked_icon_path(painter, path, color, 1.55); } static void draw_volume_icon(QPainter& painter, QColor const& color, bool muted) { QPainterPath speaker; speaker.setFillRule(Qt::WindingFill); speaker.moveTo(0, 13); speaker.lineTo(4, 13); speaker.lineTo(10, 18); speaker.lineTo(10, 0); speaker.lineTo(4, 5); speaker.lineTo(0, 5); speaker.closeSubpath(); painter.fillPath(speaker, color); if (muted) { painter.drawLine(QPoint { 0, 0 }, QPoint { 17, 18 }); return; } QPainterPath inner_wave; inner_wave.moveTo(12, 5); inner_wave.lineTo(12, 13); inner_wave.cubicTo(13.6, 13, 14.9, 11.21, 14.9, 9); inner_wave.cubicTo(14.9, 6.79, 13.6, 5, 12, 5); inner_wave.closeSubpath(); painter.fillPath(inner_wave, color); painter.setBrush(Qt::NoBrush); painter.setPen(chrome_icon_pen(color, 1.6)); painter.drawArc(QRectF(7.3, 1.4, 10.8, 15.2), 90 * 16, -180 * 16); } static QPixmap create_transparent_icon_pixmap(QSize logical_size, qreal device_pixel_ratio) { QPixmap pixmap(physical_size_for_device_pixel_ratio(logical_size, device_pixel_ratio)); pixmap.setDevicePixelRatio(device_pixel_ratio); pixmap.fill(Qt::transparent); return pixmap; } static QPixmap create_chrome_icon_pixmap(ChromeIcon icon, QColor color, qreal device_pixel_ratio) { auto pixmap = create_transparent_icon_pixmap({ 20, 20 }, device_pixel_ratio); QPainter painter(&pixmap); painter.setRenderHint(QPainter::Antialiasing, true); painter.setPen(chrome_icon_pen(color, 1.7)); switch (icon) { case ChromeIcon::Back: draw_back_icon(painter, color); break; case ChromeIcon::Forward: draw_forward_icon(painter, color); break; case ChromeIcon::Reload: draw_reload_icon(painter, color); break; case ChromeIcon::Stop: painter.setPen(chrome_icon_pen(color, 1.75)); painter.drawLine(QPointF(6.0, 6.0), QPointF(14.0, 14.0)); painter.drawLine(QPointF(14.0, 6.0), QPointF(6.0, 14.0)); break; case ChromeIcon::NewTab: painter.setPen(chrome_icon_pen(color, 1.9)); painter.drawLine(QPointF(10.0, 4.3), QPointF(10.0, 15.7)); painter.drawLine(QPointF(4.3, 10.0), QPointF(15.7, 10.0)); break; case ChromeIcon::Close: painter.setPen(chrome_icon_pen(color, 1.9)); painter.drawLine(QPointF(5.5, 7.15), QPointF(14.5, 16.65)); painter.drawLine(QPointF(14.5, 7.15), QPointF(5.5, 16.65)); break; case ChromeIcon::TabClose: painter.setPen(chrome_icon_pen(color, 1.9)); painter.drawLine(QPointF(5.5, 5.65), QPointF(14.5, 15.15)); painter.drawLine(QPointF(14.5, 5.65), QPointF(5.5, 15.15)); break; case ChromeIcon::Menu: painter.setPen(chrome_icon_pen(color, 1.75)); painter.drawLine(QPointF(4.1, 6.2), QPointF(15.9, 6.2)); painter.drawLine(QPointF(4.1, 10.0), QPointF(15.9, 10.0)); painter.drawLine(QPointF(4.1, 13.8), QPointF(15.9, 13.8)); break; case ChromeIcon::Star: draw_star_icon(painter, color, false); break; case ChromeIcon::StarFilled: draw_star_icon(painter, color, true); break; case ChromeIcon::Search: painter.setPen(chrome_icon_pen(color, 1.55)); painter.drawEllipse(QRectF(4.2, 4.0, 9.7, 9.7)); painter.drawLine(QPointF(12.1, 12.1), QPointF(16.0, 16.0)); break; case ChromeIcon::Globe: draw_globe_icon(painter, color); break; case ChromeIcon::Folder: { painter.setPen(chrome_icon_pen(color, 1.6)); QPainterPath path; path.moveTo(2.8, 6.1); path.lineTo(7.6, 6.1); path.lineTo(9.2, 8.0); path.lineTo(17.2, 8.0); path.quadTo(18.0, 8.0, 18.0, 8.8); path.lineTo(18.0, 15.1); path.quadTo(18.0, 15.9, 17.2, 15.9); path.lineTo(2.8, 15.9); path.quadTo(2.0, 15.9, 2.0, 15.1); path.lineTo(2.0, 6.9); path.quadTo(2.0, 6.1, 2.8, 6.1); path.closeSubpath(); painter.drawPath(path); break; } case ChromeIcon::Volume: draw_volume_icon(painter, color, false); break; case ChromeIcon::VolumeMuted: draw_volume_icon(painter, color, true); break; case ChromeIcon::ChevronUp: painter.setPen(chrome_icon_pen(color, 1.85)); painter.drawLine(QPointF(5.0, 12.4), QPointF(10.0, 7.4)); painter.drawLine(QPointF(10.0, 7.4), QPointF(15.0, 12.4)); break; case ChromeIcon::ChevronDown: painter.setPen(chrome_icon_pen(color, 1.85)); painter.drawLine(QPointF(5.0, 7.6), QPointF(10.0, 12.6)); painter.drawLine(QPointF(10.0, 12.6), QPointF(15.0, 7.6)); break; case ChromeIcon::VerticalTabBarCollapse: draw_vertical_tab_bar_icon(painter, color, true); break; case ChromeIcon::VerticalTabBarExpand: draw_vertical_tab_bar_icon(painter, color, false); break; case ChromeIcon::WindowMinimize: painter.setPen(chrome_icon_pen(color, 1.65)); painter.drawLine(QPointF(5.2, 12.5), QPointF(14.8, 12.5)); break; case ChromeIcon::WindowMaximize: painter.setPen(chrome_icon_pen(color, 1.55)); painter.drawRoundedRect(QRectF(5.0, 5.0, 10.0, 10.0), 1.4, 1.4); break; case ChromeIcon::WindowRestore: painter.setPen(chrome_icon_pen(color, 1.55)); painter.drawRoundedRect(QRectF(5.1, 7.0, 7.9, 7.9), 1.2, 1.2); painter.drawLine(QPointF(7.2, 5.1), QPointF(14.9, 5.1)); painter.drawLine(QPointF(14.9, 5.1), QPointF(14.9, 12.9)); break; case ChromeIcon::WindowClose: painter.setPen(chrome_icon_pen(color, 1.65)); painter.drawLine(QPointF(5.5, 5.5), QPointF(14.5, 14.5)); painter.drawLine(QPointF(14.5, 5.5), QPointF(5.5, 14.5)); break; } return pixmap; } static QIcon create_y_offset_icon(QIcon const& source, int y_offset) { constexpr int icon_size = 20; auto create_pixmap = [&](QIcon::Mode mode, int device_pixel_ratio) { auto pixmap = create_transparent_icon_pixmap({ icon_size, icon_size }, device_pixel_ratio); QPainter painter(&pixmap); painter.drawPixmap( QRect(0, y_offset, icon_size, icon_size), source.pixmap({ icon_size, icon_size }, static_cast(device_pixel_ratio), mode)); return pixmap; }; QIcon icon; for (auto device_pixel_ratio : ICON_DEVICE_PIXEL_RATIOS) { icon.addPixmap(create_pixmap(QIcon::Normal, device_pixel_ratio), QIcon::Normal); icon.addPixmap(create_pixmap(QIcon::Active, device_pixel_ratio), QIcon::Active); icon.addPixmap(create_pixmap(QIcon::Disabled, device_pixel_ratio), QIcon::Disabled); icon.addPixmap(create_pixmap(QIcon::Selected, device_pixel_ratio), QIcon::Selected); } return icon; } QIcon create_chrome_icon(ChromeIcon icon, QPalette const& palette) { auto normal = ChromeStyle::chrome_button_text(palette); auto normal_alpha = 216; if (icon == ChromeIcon::Close || icon == ChromeIcon::TabClose) normal_alpha = 172; else if (icon == ChromeIcon::Star) normal_alpha = 204; else if (icon == ChromeIcon::Globe || icon == ChromeIcon::Search || icon == ChromeIcon::Menu) normal_alpha = 188; normal.setAlpha(normal_alpha); auto active = ChromeStyle::chrome_button_text(palette); active.setAlpha(icon == ChromeIcon::Close || icon == ChromeIcon::TabClose ? 220 : 236); auto disabled = ChromeStyle::chrome_muted_text(palette); disabled.setAlpha(icon == ChromeIcon::Close || icon == ChromeIcon::TabClose ? 78 : 96); QIcon qicon; for (auto device_pixel_ratio : ICON_DEVICE_PIXEL_RATIOS) { qicon.addPixmap(create_chrome_icon_pixmap(icon, normal, device_pixel_ratio), QIcon::Normal); qicon.addPixmap(create_chrome_icon_pixmap(icon, active, device_pixel_ratio), QIcon::Active); qicon.addPixmap(create_chrome_icon_pixmap(icon, disabled, device_pixel_ratio), QIcon::Disabled); qicon.addPixmap(create_chrome_icon_pixmap(icon, active, device_pixel_ratio), QIcon::Selected); } if (icon == ChromeIcon::Back || icon == ChromeIcon::Forward || icon == ChromeIcon::NewTab) return create_y_offset_icon(qicon, -1); return qicon; } static QPixmap create_loading_spinner_icon_pixmap(QPalette const& palette, int frame, int device_pixel_ratio) { static constexpr int icon_size = 16; static constexpr int segment_count = 12; auto pixmap = create_transparent_icon_pixmap({ icon_size, icon_size }, device_pixel_ratio); QPainter painter(&pixmap); painter.setRenderHint(QPainter::Antialiasing); painter.translate(icon_size / 2.0, icon_size / 2.0); auto color = ChromeStyle::chrome_text(palette); for (int segment = 0; segment < segment_count; ++segment) { auto segment_color = color; segment_color.setAlpha(((segment - frame + segment_count) % segment_count + 1) * 255 / segment_count); painter.save(); painter.rotate(segment * 360.0 / segment_count); painter.setPen(QPen(segment_color, 2, Qt::SolidLine, Qt::RoundCap)); painter.drawLine(QPointF(0, -4), QPointF(0, -7)); painter.restore(); } return pixmap; } QIcon loading_spinner_icon(QPalette const& palette, int frame) { QIcon icon; for (auto device_pixel_ratio : ICON_DEVICE_PIXEL_RATIOS) icon.addPixmap(create_loading_spinner_icon_pixmap(palette, frame, device_pixel_ratio)); return icon; } QSize physical_size_for_device_pixel_ratio(QSize size, qreal device_pixel_ratio) { return { static_cast(ceil(size.width() * device_pixel_ratio)), static_cast(ceil(size.height() * device_pixel_ratio)), }; } }