From 3110f5b32869e2e7cce08d226400f33152429229 Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Mon, 9 Jan 2023 00:44:32 +0100 Subject: [PATCH] Presenter: Rearchitect on top of LibWeb This patch replaces the bespoke rendering engine in Presenter with a simple pipeline that turns presentations into single-page HTML files. The HTML is then loaded into an OutOfProcessWebView. This achieves a number of things, most importantly: - Access to all the CSS features supported by LibWeb - Sandboxed, multi-process rendering The code could be simplified a lot further, but I wanted to get the new architecture in place without changing anything about the file format. --- .../Applications/Presenter/CMakeLists.txt | 4 +- .../Applications/Presenter/Presentation.cpp | 55 +++-- .../Applications/Presenter/Presentation.h | 9 +- .../Presenter/PresenterWidget.cpp | 79 ++++--- .../Applications/Presenter/PresenterWidget.h | 8 + Userland/Applications/Presenter/Slide.cpp | 25 +- Userland/Applications/Presenter/Slide.h | 8 +- .../Applications/Presenter/SlideObject.cpp | 215 +++++++++--------- Userland/Applications/Presenter/SlideObject.h | 129 +++-------- Userland/Applications/Presenter/main.cpp | 3 +- 10 files changed, 267 insertions(+), 268 deletions(-) diff --git a/Userland/Applications/Presenter/CMakeLists.txt b/Userland/Applications/Presenter/CMakeLists.txt index 70976d4596..7fd33b82b1 100644 --- a/Userland/Applications/Presenter/CMakeLists.txt +++ b/Userland/Applications/Presenter/CMakeLists.txt @@ -2,7 +2,7 @@ serenity_component( Presenter RECOMMENDED TARGETS Presenter - DEPENDS ImageDecoder FileSystemAccessServer + DEPENDS FileSystemAccessServer ) @@ -14,4 +14,4 @@ set(SOURCES SlideObject.cpp ) serenity_app(Presenter ICON app-display-settings) -target_link_libraries(Presenter PRIVATE LibImageDecoderClient LibGUI LibGfx LibFileSystemAccessClient LibCore LibMain) +target_link_libraries(Presenter PRIVATE LibWebView LibGUI LibGfx LibFileSystemAccessClient LibCore LibMain) diff --git a/Userland/Applications/Presenter/Presentation.cpp b/Userland/Applications/Presenter/Presentation.cpp index bb12f74945..cdd6f7321f 100644 --- a/Userland/Applications/Presenter/Presentation.cpp +++ b/Userland/Applications/Presenter/Presentation.cpp @@ -1,15 +1,14 @@ /* * Copyright (c) 2022, kleines Filmröllchen + * Copyright (c) 2023, Andreas Kling * * SPDX-License-Identifier: BSD-2-Clause */ #include "Presentation.h" -#include #include #include #include -#include #include Presentation::Presentation(Gfx::IntSize normative_size, HashMap metadata) @@ -66,7 +65,7 @@ void Presentation::go_to_first_slide() m_current_slide = 0; } -ErrorOr> Presentation::load_from_file(StringView file_name, NonnullRefPtr window) +ErrorOr> Presentation::load_from_file(StringView file_name) { if (file_name.is_empty()) return ENOENT; @@ -104,7 +103,7 @@ ErrorOr> Presentation::load_from_file(StringView fil return Error::from_string_view("Slides must be objects"sv); auto const& slide_object = maybe_slide.as_object(); - auto slide = TRY(Slide::parse_slide(slide_object, window)); + auto slide = TRY(Slide::parse_slide(slide_object)); presentation->append_slide(move(slide)); } @@ -147,15 +146,45 @@ ErrorOr Presentation::parse_presentation_size(JsonObject const& me }; } -void Presentation::paint(Gfx::Painter& painter) const +ErrorOr Presentation::render() { - auto display_area = painter.clip_rect(); - // These two should be the same, but better be safe than sorry. - auto width_scale = static_cast(display_area.width()) / static_cast(m_normative_size.width()); - auto height_scale = static_cast(display_area.height()) / static_cast(m_normative_size.height()); - auto scale = Gfx::FloatSize { static_cast(width_scale), static_cast(height_scale) }; + HTMLElement main_element; + main_element.tag_name = "main"sv; + for (size_t i = 0; i < m_slides.size(); ++i) { + HTMLElement slide_div; + slide_div.tag_name = "div"sv; + TRY(slide_div.style.try_set("display"sv, "none"sv)); + TRY(slide_div.attributes.try_set("id"sv, DeprecatedString::formatted("slide{}", i))); + TRY(slide_div.attributes.try_set("class"sv, "slide")); + auto& slide = m_slides[i]; + TRY(slide_div.children.try_append(TRY(slide.render(*this)))); + main_element.children.append(move(slide_div)); + } - // FIXME: Fill the background with a color depending on the color scheme - painter.clear_rect(painter.clip_rect(), Color::White); - current_slide().paint(painter, m_current_frame_in_slide.value(), scale); + StringBuilder builder; + TRY(builder.try_append(R"( + +)"sv)); + TRY(main_element.serialize(builder)); + TRY(builder.try_append(""sv)); + return builder.to_deprecated_string(); } diff --git a/Userland/Applications/Presenter/Presentation.h b/Userland/Applications/Presenter/Presentation.h index 6ad200811d..f2e6084d0a 100644 --- a/Userland/Applications/Presenter/Presentation.h +++ b/Userland/Applications/Presenter/Presentation.h @@ -1,5 +1,6 @@ /* * Copyright (c) 2022, kleines Filmröllchen + * Copyright (c) 2023, Andreas Kling * * SPDX-License-Identifier: BSD-2-Clause */ @@ -8,11 +9,9 @@ #include "Slide.h" #include -#include #include #include #include -#include #include static constexpr int const PRESENTATION_FORMAT_VERSION = 1; @@ -23,8 +22,7 @@ class Presentation { public: ~Presentation() = default; - // We can't pass this class directly in an ErrorOr because some of the components are not properly moveable under these conditions. - static ErrorOr> load_from_file(StringView file_name, NonnullRefPtr window); + static ErrorOr> load_from_file(StringView file_name); StringView title() const; StringView author() const; @@ -38,8 +36,7 @@ public: void previous_frame(); void go_to_first_slide(); - // This assumes that the caller has clipped the painter to exactly the display area. - void paint(Gfx::Painter& painter) const; + ErrorOr render(); private: static HashMap parse_metadata(JsonObject const& metadata_object); diff --git a/Userland/Applications/Presenter/PresenterWidget.cpp b/Userland/Applications/Presenter/PresenterWidget.cpp index 652bdaacd4..66a6a5539b 100644 --- a/Userland/Applications/Presenter/PresenterWidget.cpp +++ b/Userland/Applications/Presenter/PresenterWidget.cpp @@ -1,12 +1,12 @@ /* * Copyright (c) 2022, kleines Filmröllchen + * Copyright (c) 2023, Andreas Kling * * SPDX-License-Identifier: BSD-2-Clause */ #include "PresenterWidget.h" #include "Presentation.h" -#include #include #include #include @@ -15,13 +15,39 @@ #include #include #include -#include -#include -#include PresenterWidget::PresenterWidget() { set_min_size(100, 100); + set_fill_with_background_color(true); + m_web_view = add(); + m_web_view->set_frame_thickness(0); + m_web_view->set_scrollbars_enabled(false); + m_web_view->set_focus_policy(GUI::FocusPolicy::NoFocus); + m_web_view->set_content_scales_to_viewport(true); +} + +void PresenterWidget::resize_event(GUI::ResizeEvent& event) +{ + Widget::resize_event(event); + + if (!m_current_presentation) + return; + + auto normative_size = m_current_presentation->normative_size().to_type(); + float widget_ratio = static_cast(event.size().width()) / static_cast(event.size().height()); + float wh_ratio = normative_size.width() / normative_size.height(); + + Gfx::IntRect rect; + if (widget_ratio >= wh_ratio) { + rect.set_width(static_cast(ceilf(static_cast(event.size().height()) * wh_ratio))); + rect.set_height(event.size().height()); + } else { + float hw_ratio = normative_size.height() / normative_size.width(); + rect.set_width(event.size().width()); + rect.set_height(static_cast(ceilf(static_cast(event.size().width()) * hw_ratio))); + } + m_web_view->set_relative_rect(rect.centered_within(this->rect())); } ErrorOr PresenterWidget::initialize_menubar() @@ -44,15 +70,13 @@ ErrorOr PresenterWidget::initialize_menubar() auto next_slide_action = GUI::Action::create("&Next", { KeyCode::Key_Right }, TRY(Gfx::Bitmap::try_load_from_file("/res/icons/16x16/go-forward.png"sv)), [this](auto&) { if (m_current_presentation) { m_current_presentation->next_frame(); - outln("Switched forward to slide {} frame {}", m_current_presentation->current_slide_number(), m_current_presentation->current_frame_in_slide_number()); - update(); + update_web_view(); } }); auto previous_slide_action = GUI::Action::create("&Previous", { KeyCode::Key_Left }, TRY(Gfx::Bitmap::try_load_from_file("/res/icons/16x16/go-back.png"sv)), [this](auto&) { if (m_current_presentation) { m_current_presentation->previous_frame(); - outln("Switched backward to slide {} frame {}", m_current_presentation->current_slide_number(), m_current_presentation->current_frame_in_slide_number()); - update(); + update_web_view(); } }); TRY(presentation_menu.try_add_action(next_slide_action)); @@ -64,25 +88,31 @@ ErrorOr PresenterWidget::initialize_menubar() this->window()->set_fullscreen(true); }))); TRY(presentation_menu.try_add_action(GUI::Action::create("Present From First &Slide", { KeyCode::Key_F5 }, TRY(Gfx::Bitmap::try_load_from_file("/res/icons/16x16/play.png"sv)), [this](auto&) { - if (m_current_presentation) + if (m_current_presentation) { m_current_presentation->go_to_first_slide(); + update_web_view(); + } this->window()->set_fullscreen(true); }))); return {}; } +void PresenterWidget::update_web_view() +{ + m_web_view->run_javascript(DeprecatedString::formatted("goto({}, {})", m_current_presentation->current_slide_number(), m_current_presentation->current_frame_in_slide_number())); +} + void PresenterWidget::set_file(StringView file_name) { - auto presentation = Presentation::load_from_file(file_name, *window()); + auto presentation = Presentation::load_from_file(file_name); if (presentation.is_error()) { GUI::MessageBox::show_error(window(), DeprecatedString::formatted("The presentation \"{}\" could not be loaded.\n{}", file_name, presentation.error())); } else { m_current_presentation = presentation.release_value(); window()->set_title(DeprecatedString::formatted(title_template, m_current_presentation->title(), m_current_presentation->author())); set_min_size(m_current_presentation->normative_size()); - // This will apply the new minimum size. - update(); + m_web_view->load_html(MUST(m_current_presentation->render()), "presenter://slide.html"sv); } } @@ -114,24 +144,19 @@ void PresenterWidget::keydown_event(GUI::KeyEvent& event) } } -void PresenterWidget::paint_event([[maybe_unused]] GUI::PaintEvent& event) +void PresenterWidget::paint_event(GUI::PaintEvent& event) +{ + GUI::Painter painter(*this); + painter.clear_rect(event.rect(), Gfx::Color::Black); +} + +void PresenterWidget::second_paint_event(GUI::PaintEvent& event) { if (!m_current_presentation) return; - auto normative_size = m_current_presentation->normative_size(); - // Choose an aspect-correct size which doesn't exceed actual widget dimensions. - auto width_corresponding_to_height = height() * normative_size.aspect_ratio(); - auto dimension_to_preserve = (width_corresponding_to_height > width()) ? Orientation::Horizontal : Orientation::Vertical; - auto display_size = size().match_aspect_ratio(normative_size.aspect_ratio(), dimension_to_preserve); - - GUI::Painter painter { *this }; - auto clip_rect = Gfx::IntRect::centered_at({ width() / 2, height() / 2 }, display_size); - painter.clear_clip_rect(); - // FIXME: This currently leaves a black border when the window aspect ratio doesn't match. - // Figure out a way to apply the background color here as well. - painter.add_clip_rect(clip_rect); - - m_current_presentation->paint(painter); + GUI::Painter painter(*this); + painter.add_clip_rect(event.rect()); + painter.draw_text(m_web_view->relative_rect(), m_current_presentation->current_slide().title(), Gfx::TextAlignment::BottomCenter); } void PresenterWidget::drag_enter_event(GUI::DragEvent& event) diff --git a/Userland/Applications/Presenter/PresenterWidget.h b/Userland/Applications/Presenter/PresenterWidget.h index f3c95dc62d..e2b731f27b 100644 --- a/Userland/Applications/Presenter/PresenterWidget.h +++ b/Userland/Applications/Presenter/PresenterWidget.h @@ -1,5 +1,6 @@ /* * Copyright (c) 2022, kleines Filmröllchen + * Copyright (c) 2023, Andreas Kling * * SPDX-License-Identifier: BSD-2-Clause */ @@ -11,6 +12,7 @@ #include #include #include +#include // Title, Author constexpr StringView const title_template = "{} ({}) — Presenter"sv; @@ -29,11 +31,17 @@ public: protected: virtual void paint_event(GUI::PaintEvent&) override; + virtual void second_paint_event(GUI::PaintEvent&) override; virtual void keydown_event(GUI::KeyEvent&) override; virtual void drag_enter_event(GUI::DragEvent&) override; virtual void drop_event(GUI::DropEvent&) override; + virtual void resize_event(GUI::ResizeEvent&) override; private: + void update_web_view(); + + RefPtr m_web_view; + OwnPtr m_current_presentation; RefPtr m_next_slide_action; RefPtr m_previous_slide_action; diff --git a/Userland/Applications/Presenter/Slide.cpp b/Userland/Applications/Presenter/Slide.cpp index ff563bf7ac..1952204e1f 100644 --- a/Userland/Applications/Presenter/Slide.cpp +++ b/Userland/Applications/Presenter/Slide.cpp @@ -1,16 +1,13 @@ /* * Copyright (c) 2022, kleines Filmröllchen + * Copyright (c) 2023, Andreas Kling * * SPDX-License-Identifier: BSD-2-Clause */ #include "Slide.h" +#include "Presentation.h" #include -#include -#include -#include -#include -#include Slide::Slide(NonnullRefPtrVector slide_objects, DeprecatedString title) : m_slide_objects(move(slide_objects)) @@ -18,7 +15,7 @@ Slide::Slide(NonnullRefPtrVector slide_objects, DeprecatedString ti { } -ErrorOr Slide::parse_slide(JsonObject const& slide_json, NonnullRefPtr window) +ErrorOr Slide::parse_slide(JsonObject const& slide_json) { // FIXME: Use the text with the "title" role for a title, if there is no title given. auto title = slide_json.get("title"sv).as_string_or("Untitled slide"); @@ -34,20 +31,18 @@ ErrorOr Slide::parse_slide(JsonObject const& slide_json, NonnullRefPtr Slide::render(Presentation const& presentation) const { - for (auto const& object : m_slide_objects) { - if (object.is_visible_during_frame(current_frame)) - object.paint(painter, display_scale); - } - - // FIXME: Move this to user settings. - painter.draw_text(painter.clip_rect(), title(), Gfx::TextAlignment::BottomCenter); + HTMLElement wrapper; + wrapper.tag_name = "div"sv; + for (auto const& object : m_slide_objects) + TRY(wrapper.children.try_append(TRY(object.render(presentation)))); + return wrapper; } diff --git a/Userland/Applications/Presenter/Slide.h b/Userland/Applications/Presenter/Slide.h index 565a64109c..8e7482b47b 100644 --- a/Userland/Applications/Presenter/Slide.h +++ b/Userland/Applications/Presenter/Slide.h @@ -1,5 +1,6 @@ /* * Copyright (c) 2022, kleines Filmröllchen + * Copyright (c) 2023, Andreas Kling * * SPDX-License-Identifier: BSD-2-Clause */ @@ -9,19 +10,18 @@ #include "SlideObject.h" #include #include -#include -#include +#include // A single slide of a presentation. class Slide final { public: - static ErrorOr parse_slide(JsonObject const& slide_json, NonnullRefPtr window); + static ErrorOr parse_slide(JsonObject const& slide_json); // FIXME: shouldn't be hard-coded to 1. unsigned frame_count() const { return 1; } StringView title() const { return m_title; } - void paint(Gfx::Painter&, unsigned current_frame, Gfx::FloatSize display_scale) const; + ErrorOr render(Presentation const&) const; private: Slide(NonnullRefPtrVector slide_objects, DeprecatedString title); diff --git a/Userland/Applications/Presenter/SlideObject.cpp b/Userland/Applications/Presenter/SlideObject.cpp index 931a8e2a1c..f76aefe374 100644 --- a/Userland/Applications/Presenter/SlideObject.cpp +++ b/Userland/Applications/Presenter/SlideObject.cpp @@ -5,23 +5,20 @@ */ #include "SlideObject.h" +#include "Presentation.h" #include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include +#include +#include -ErrorOr> SlideObject::parse_slide_object(JsonObject const& slide_object_json, NonnullRefPtr window) +static DeprecatedString to_css_length(float design_value, Presentation const& presentation) { - auto image_decoder_client = TRY(ImageDecoderClient::Client::try_create()); + float length_in_vw = design_value / static_cast(presentation.normative_size().width()) * 100.0f; + return DeprecatedString::formatted("{}vw", length_in_vw); +} +ErrorOr> SlideObject::parse_slide_object(JsonObject const& slide_object_json) +{ auto const& maybe_type = slide_object_json.get("type"sv); if (!maybe_type.is_string()) return Error::from_string_view("Slide object must have a type"sv); @@ -31,127 +28,131 @@ ErrorOr> SlideObject::parse_slide_object(JsonObject c if (type == "text"sv) object = TRY(try_make_ref_counted()); else if (type == "image"sv) - object = TRY(try_make_ref_counted(image_decoder_client, window)); + object = TRY(try_make_ref_counted()); else return Error::from_string_view("Unsupported slide object type"sv); slide_object_json.for_each_member([&](auto const& key, auto const& value) { - if (key == "type"sv) - return; - auto successful = object->set_property(key, value); - if (!successful) - dbgln("Storing {:15} = {:20} on slide object type {:8} failed, ignoring.", key, value, type); + object->set_property(key, value); }); return object.release_nonnull(); } -SlideObject::SlideObject() +void SlideObject::set_property(StringView name, JsonValue value) { - REGISTER_RECT_PROPERTY("rect", rect, set_rect); + if (name == "rect"sv) { + if (value.is_array() && value.as_array().size() == 4) { + Gfx::IntRect rect; + rect.set_x(value.as_array()[0].to_i32()); + rect.set_y(value.as_array()[1].to_i32()); + rect.set_width(value.as_array()[2].to_i32()); + rect.set_height(value.as_array()[3].to_i32()); + m_rect = rect; + } + } + m_properties.set(name, move(value)); } -// FIXME: Consider drawing a placeholder box instead. -void SlideObject::paint(Gfx::Painter&, Gfx::FloatSize) const { } - -Gfx::IntRect SlideObject::transformed_bounding_box(Gfx::IntRect clip_rect, Gfx::FloatSize display_scale) const +void GraphicsObject::set_property(StringView name, JsonValue value) { - return m_rect.to_type().scaled(display_scale.width(), display_scale.height()).to_rounded().translated(clip_rect.top_left()); + if (name == "color"sv) { + if (auto color = Gfx::Color::from_string(value.to_deprecated_string()); color.has_value()) { + m_color = color.release_value(); + } + } + SlideObject::set_property(name, move(value)); } -GraphicsObject::GraphicsObject() +void Text::set_property(StringView name, JsonValue value) { - register_property( - "color", [this]() { return this->color().to_deprecated_string(); }, - [this](auto& value) { - auto color = Color::from_string(value.to_deprecated_string()); - if (color.has_value()) { - this->set_color(color.value()); - return true; - } - return false; - }); + if (name == "text"sv) { + m_text = value.to_deprecated_string(); + } else if (name == "font"sv) { + m_font_family = value.to_deprecated_string(); + } else if (name == "font-weight"sv) { + m_font_weight = Gfx::name_to_weight(value.to_deprecated_string()); + } else if (name == "font-size"sv) { + m_font_size_in_pt = value.to_float(); + } else if (name == "text-alignment"sv) { + m_text_align = value.to_deprecated_string(); + } + GraphicsObject::set_property(name, move(value)); } -Text::Text() +void Image::set_property(StringView name, JsonValue value) { - REGISTER_STRING_PROPERTY("text", text, set_text); - REGISTER_FONT_WEIGHT_PROPERTY("font-weight", font_weight, set_font_weight); - REGISTER_TEXT_ALIGNMENT_PROPERTY("text-alignment", text_alignment, set_text_alignment); - REGISTER_INT_PROPERTY("font-size", font_size, set_font_size); - REGISTER_STRING_PROPERTY("font", font, set_font); + if (name == "path"sv) { + m_src = value.to_deprecated_string(); + } else if (name == "scaling-mode"sv) { + if (value.to_deprecated_string() == "nearest-neighbor"sv) + m_image_rendering = "crisp-edges"sv; + else if (value.to_deprecated_string() == "smooth-pixels"sv) + m_image_rendering = "pixelated"sv; + } + SlideObject::set_property(name, move(value)); } -void Text::paint(Gfx::Painter& painter, Gfx::FloatSize display_scale) const +ErrorOr Text::render(Presentation const& presentation) const { - auto scaled_bounding_box = this->transformed_bounding_box(painter.clip_rect(), display_scale); - - auto scaled_font_size = display_scale.height() * static_cast(m_font_size); - auto font = Gfx::FontDatabase::the().get(m_font, scaled_font_size, m_font_weight, 0, Gfx::Font::AllowInexactSizeMatch::Yes); - if (font.is_null()) - font = Gfx::FontDatabase::default_font(); - - painter.draw_text(scaled_bounding_box, m_text.view(), *font, m_text_alignment, m_color, Gfx::TextElision::None, Gfx::TextWrapping::Wrap); + HTMLElement div; + div.tag_name = "div"sv; + div.style.set("color"sv, m_color.to_deprecated_string()); + div.style.set("font-family"sv, DeprecatedString::formatted("'{}'", m_font_family)); + div.style.set("font-size"sv, to_css_length(m_font_size_in_pt * 1.33333333f, presentation)); + div.style.set("font-weight"sv, DeprecatedString::number(m_font_weight)); + div.style.set("text-align"sv, m_text_align); + div.style.set("white-space"sv, "pre-wrap"sv); + div.style.set("width"sv, to_css_length(m_rect.width(), presentation)); + div.style.set("height"sv, to_css_length(m_rect.height(), presentation)); + div.style.set("position"sv, "absolute"sv); + div.style.set("left"sv, to_css_length(m_rect.left(), presentation)); + div.style.set("top"sv, to_css_length(m_rect.top(), presentation)); + div.inner_text = m_text; + return div; } -Image::Image(NonnullRefPtr client, NonnullRefPtr window) - : m_client(move(client)) - , m_window(move(window)) +ErrorOr Image::render(Presentation const& presentation) const { - REGISTER_STRING_PROPERTY("path", image_path, set_image_path); - REGISTER_ENUM_PROPERTY("scaling", scaling, set_scaling, ImageScaling, - { ImageScaling::FitSmallest, "fit-smallest" }, - { ImageScaling::FitLargest, "fit-largest" }, - { ImageScaling::Stretch, "stretch" }, ); - REGISTER_ENUM_PROPERTY("scaling-mode", scaling_mode, set_scaling_mode, Gfx::Painter::ScalingMode, - { Gfx::Painter::ScalingMode::SmoothPixels, "smooth-pixels" }, - { Gfx::Painter::ScalingMode::NearestNeighbor, "nearest-neighbor" }, - { Gfx::Painter::ScalingMode::BilinearBlend, "bilinear-blend" }, ); + HTMLElement img; + img.tag_name = "img"sv; + img.attributes.set("src"sv, URL::create_with_file_scheme(m_src).to_deprecated_string()); + img.style.set("image-rendering"sv, m_image_rendering); + if (m_rect.width() > m_rect.height()) + img.style.set("height"sv, "100%"sv); + else + img.style.set("width"sv, "100%"sv); + + HTMLElement image_wrapper; + image_wrapper.tag_name = "div"sv; + image_wrapper.children.append(move(img)); + image_wrapper.style.set("position"sv, "absolute"sv); + image_wrapper.style.set("left"sv, to_css_length(m_rect.left(), presentation)); + image_wrapper.style.set("top"sv, to_css_length(m_rect.top(), presentation)); + image_wrapper.style.set("width"sv, to_css_length(m_rect.width(), presentation)); + image_wrapper.style.set("height"sv, to_css_length(m_rect.height(), presentation)); + image_wrapper.style.set("text-align"sv, "center"sv); + return image_wrapper; } -// FIXME: Should run on another thread and report errors. -ErrorOr Image::reload_image() +ErrorOr HTMLElement::serialize(StringBuilder& builder) const { - auto file = TRY(Core::Stream::File::open(m_image_path, Core::Stream::OpenMode::Read)); - auto data = TRY(file->read_until_eof()); - auto maybe_decoded = m_client->decode_image(data); - if (!maybe_decoded.has_value() || maybe_decoded.value().frames.size() < 1) - return Error::from_string_view("Could not decode image"sv); - // FIXME: Handle multi-frame images. - m_currently_loaded_image = maybe_decoded.value().frames.first().bitmap; + TRY(builder.try_appendff("<{}", tag_name)); + for (auto const& [key, value] : attributes) { + // FIXME: Escape the value string as necessary. + TRY(builder.try_appendff(" {}='{}'", key, value)); + } + TRY(builder.try_append(" style=\""sv)); + for (auto const& [key, value] : style) { + // FIXME: Escape the value string as necessary. + TRY(builder.try_appendff(" {}: {};", key, value)); + } + TRY(builder.try_append("\">"sv)); + if (!inner_text.is_empty()) + TRY(builder.try_append(inner_text)); + for (auto const& child : children) { + TRY(child.serialize(builder)); + } + TRY(builder.try_appendff("", tag_name)); return {}; } - -void Image::paint(Gfx::Painter& painter, Gfx::FloatSize display_scale) const -{ - if (!m_currently_loaded_image) - return; - - auto transformed_bounding_box = this->transformed_bounding_box(painter.clip_rect(), display_scale); - - auto image_size = m_currently_loaded_image->size(); - auto image_aspect_ratio = image_size.aspect_ratio(); - - auto image_box = transformed_bounding_box; - if (m_scaling != ImageScaling::Stretch) { - auto width_corresponding_to_height = image_box.height() * image_aspect_ratio; - auto direction_to_preserve_for_fit = width_corresponding_to_height > image_box.width() ? Orientation::Horizontal : Orientation::Vertical; - // Fit largest and fit smallest are the same, except with inverted preservation conditions. - if (m_scaling == ImageScaling::FitLargest) - direction_to_preserve_for_fit = direction_to_preserve_for_fit == Orientation::Vertical ? Orientation::Horizontal : Orientation::Vertical; - - image_box.set_size(image_box.size().match_aspect_ratio(image_aspect_ratio, direction_to_preserve_for_fit)); - } - - image_box = image_box.centered_within(transformed_bounding_box); - - auto original_clip_rect = painter.clip_rect(); - painter.clear_clip_rect(); - painter.add_clip_rect(image_box); - - // FIXME: Allow to set the scaling mode. - painter.draw_scaled_bitmap(image_box, *m_currently_loaded_image, m_currently_loaded_image->rect(), 1.0f, m_scaling_mode); - - painter.clear_clip_rect(); - painter.add_clip_rect(original_clip_rect); -} diff --git a/Userland/Applications/Presenter/SlideObject.h b/Userland/Applications/Presenter/SlideObject.h index a36bde85e0..0ace94b7d3 100644 --- a/Userland/Applications/Presenter/SlideObject.h +++ b/Userland/Applications/Presenter/SlideObject.h @@ -6,133 +6,76 @@ #pragma once -#include -#include -#include -#include -#include -#include -#include #include -#include -#include -#include +#include #include -#include -#include + +class Presentation; + +struct HTMLElement { + StringView tag_name; + HashMap attributes; + HashMap style; + DeprecatedString inner_text; + Vector children; + + ErrorOr serialize(StringBuilder&) const; +}; // Anything that can be on a slide. -// For properties set in the file, we re-use the Core::Object property facility. -class SlideObject : public Core::Object { - C_OBJECT_ABSTRACT(SlideObject); - +class SlideObject : public RefCounted { public: virtual ~SlideObject() = default; - - static ErrorOr> parse_slide_object(JsonObject const& slide_object_json, NonnullRefPtr window); - - // FIXME: Actually determine this from the file data. - bool is_visible_during_frame([[maybe_unused]] unsigned frame_number) const { return true; } - - virtual void paint(Gfx::Painter&, Gfx::FloatSize display_scale) const; - ALWAYS_INLINE Gfx::IntRect transformed_bounding_box(Gfx::IntRect clip_rect, Gfx::FloatSize display_scale) const; - - void set_rect(Gfx::IntRect rect) { m_rect = rect; } - Gfx::IntRect rect() const { return m_rect; } + static ErrorOr> parse_slide_object(JsonObject const& slide_object_json); + virtual ErrorOr render(Presentation const&) const = 0; protected: - SlideObject(); + SlideObject() = default; + virtual void set_property(StringView name, JsonValue); + + HashMap m_properties; Gfx::IntRect m_rect; }; // Objects with a foreground color. class GraphicsObject : public SlideObject { - C_OBJECT_ABSTRACT(SlideObject); - public: virtual ~GraphicsObject() = default; - void set_color(Gfx::Color color) { m_color = color; } - Gfx::Color color() const { return m_color; } - protected: - GraphicsObject(); + GraphicsObject() = default; + virtual void set_property(StringView name, JsonValue) override; // FIXME: Change the default color based on the color scheme Gfx::Color m_color { Gfx::Color::Black }; }; -class Text : public GraphicsObject { - C_OBJECT(SlideObject); - +class Text final : public GraphicsObject { public: - Text(); + Text() = default; virtual ~Text() = default; - virtual void paint(Gfx::Painter&, Gfx::FloatSize display_scale) const override; +private: + virtual ErrorOr render(Presentation const&) const override; + virtual void set_property(StringView name, JsonValue) override; - void set_font(DeprecatedString font) { m_font = move(font); } - StringView font() const { return m_font; } - void set_font_size(int font_size) { m_font_size = font_size; } - int font_size() const { return m_font_size; } - void set_font_weight(unsigned font_weight) { m_font_weight = font_weight; } - unsigned font_weight() const { return m_font_weight; } - void set_text_alignment(Gfx::TextAlignment text_alignment) { m_text_alignment = text_alignment; } - Gfx::TextAlignment text_alignment() const { return m_text_alignment; } - void set_text(DeprecatedString text) { m_text = move(text); } - StringView text() const { return m_text; } - -protected: DeprecatedString m_text; - // The font family, technically speaking. - DeprecatedString m_font; - int m_font_size { 18 }; + DeprecatedString m_font_family; + DeprecatedString m_text_align; + float m_font_size_in_pt { 18 }; unsigned m_font_weight { Gfx::FontWeight::Regular }; - Gfx::TextAlignment m_text_alignment { Gfx::TextAlignment::CenterLeft }; }; -// How to scale an image object. -enum class ImageScaling { - // Fit the image into the bounding box, preserving its aspect ratio. - FitSmallest, - // Match the bounding box in width and height exactly; this will change the image's aspect ratio if the aspect ratio of the bounding box is not exactly the same. - Stretch, - // Make the image fill the bounding box, preserving its aspect ratio. This means that the image will be cut off on the top and bottom or left and right, depending on which dimension is "too large". - FitLargest, -}; - -class Image : public SlideObject { - C_OBJECT(Image); - +class Image final : public SlideObject { public: - Image(NonnullRefPtr, NonnullRefPtr); + Image() = default; virtual ~Image() = default; - virtual void paint(Gfx::Painter&, Gfx::FloatSize display_scale) const override; - - void set_image_path(DeprecatedString image_path) - { - m_image_path = move(image_path); - auto result = reload_image(); - if (result.is_error()) - GUI::MessageBox::show_error(m_window, DeprecatedString::formatted("Loading image {} failed: {}", m_image_path, result.error())); - } - StringView image_path() const { return m_image_path; } - void set_scaling(ImageScaling scaling) { m_scaling = scaling; } - ImageScaling scaling() const { return m_scaling; } - void set_scaling_mode(Gfx::Painter::ScalingMode scaling_mode) { m_scaling_mode = scaling_mode; } - Gfx::Painter::ScalingMode scaling_mode() const { return m_scaling_mode; } - -protected: - DeprecatedString m_image_path; - ImageScaling m_scaling { ImageScaling::FitSmallest }; - Gfx::Painter::ScalingMode m_scaling_mode { Gfx::Painter::ScalingMode::SmoothPixels }; - private: - ErrorOr reload_image(); + DeprecatedString m_src; + StringView m_image_rendering; - RefPtr m_currently_loaded_image; - NonnullRefPtr m_client; - NonnullRefPtr m_window; + virtual ErrorOr render(Presentation const&) const override; + virtual void set_property(StringView name, JsonValue) override; }; diff --git a/Userland/Applications/Presenter/main.cpp b/Userland/Applications/Presenter/main.cpp index 71d2c41cd9..d97f733c14 100644 --- a/Userland/Applications/Presenter/main.cpp +++ b/Userland/Applications/Presenter/main.cpp @@ -1,5 +1,6 @@ /* * Copyright (c) 2022, kleines Filmröllchen + * Copyright (c) 2023, Andreas Kling * * SPDX-License-Identifier: BSD-2-Clause */ @@ -14,7 +15,7 @@ ErrorOr serenity_main(Main::Arguments arguments) { - // rpath is required to load .presenter files, unix, sendfd and recvfd are required to talk to ImageDecoder and WindowServer. + // rpath is required to load .presenter files, unix, sendfd and recvfd are required to talk to WindowServer and WebContent. TRY(Core::System::pledge("stdio rpath unix sendfd recvfd")); DeprecatedString file_to_load;