diff options
author | Andreas Kling <kling@serenityos.org> | 2023-01-09 00:44:32 +0100 |
---|---|---|
committer | Linus Groh <mail@linusgroh.de> | 2023-01-09 22:34:57 +0000 |
commit | 3110f5b32869e2e7cce08d226400f33152429229 (patch) | |
tree | 14875b1cafaf9dc143e102fc4673bbf0b9ff0e94 /Userland/Applications/Presenter | |
parent | ed3c2cbdf6e29e545fabfb862bbbbfaf05bb1ad6 (diff) | |
download | serenity-3110f5b32869e2e7cce08d226400f33152429229.zip |
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.
Diffstat (limited to 'Userland/Applications/Presenter')
-rw-r--r-- | Userland/Applications/Presenter/CMakeLists.txt | 4 | ||||
-rw-r--r-- | Userland/Applications/Presenter/Presentation.cpp | 57 | ||||
-rw-r--r-- | Userland/Applications/Presenter/Presentation.h | 9 | ||||
-rw-r--r-- | Userland/Applications/Presenter/PresenterWidget.cpp | 79 | ||||
-rw-r--r-- | Userland/Applications/Presenter/PresenterWidget.h | 8 | ||||
-rw-r--r-- | Userland/Applications/Presenter/Slide.cpp | 25 | ||||
-rw-r--r-- | Userland/Applications/Presenter/Slide.h | 8 | ||||
-rw-r--r-- | Userland/Applications/Presenter/SlideObject.cpp | 215 | ||||
-rw-r--r-- | Userland/Applications/Presenter/SlideObject.h | 129 | ||||
-rw-r--r-- | Userland/Applications/Presenter/main.cpp | 3 |
10 files changed, 268 insertions, 269 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 <filmroellchen@serenityos.org> + * Copyright (c) 2023, Andreas Kling <kling@serenityos.org> * * SPDX-License-Identifier: BSD-2-Clause */ #include "Presentation.h" -#include <AK/Forward.h> #include <AK/JsonObject.h> #include <LibCore/Stream.h> #include <LibGUI/Window.h> -#include <LibGfx/Forward.h> #include <errno_codes.h> Presentation::Presentation(Gfx::IntSize normative_size, HashMap<DeprecatedString, DeprecatedString> metadata) @@ -66,7 +65,7 @@ void Presentation::go_to_first_slide() m_current_slide = 0; } -ErrorOr<NonnullOwnPtr<Presentation>> Presentation::load_from_file(StringView file_name, NonnullRefPtr<GUI::Window> window) +ErrorOr<NonnullOwnPtr<Presentation>> Presentation::load_from_file(StringView file_name) { if (file_name.is_empty()) return ENOENT; @@ -104,7 +103,7 @@ ErrorOr<NonnullOwnPtr<Presentation>> 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<Gfx::IntSize> Presentation::parse_presentation_size(JsonObject const& me }; } -void Presentation::paint(Gfx::Painter& painter) const +ErrorOr<DeprecatedString> 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<double>(display_area.width()) / static_cast<double>(m_normative_size.width()); - auto height_scale = static_cast<double>(display_area.height()) / static_cast<double>(m_normative_size.height()); - auto scale = Gfx::FloatSize { static_cast<float>(width_scale), static_cast<float>(height_scale) }; - - // 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); + 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)); + } + + StringBuilder builder; + TRY(builder.try_append(R"( +<!DOCTYPE html><html><head><style> + .slide { + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + } +</style><script> + function goto(slideIndex, frameIndex) { + // FIXME: Honor the frameIndex. + let slide; + for (slide of document.getElementsByClassName("slide")) { + slide.style.display = "none"; + } + if (slide = document.getElementById(`slide${slideIndex}`)) + slide.style.display = "block"; + } + window.onload = function() { goto(0, 0) } +</script><body> +)"sv)); + TRY(main_element.serialize(builder)); + TRY(builder.try_append("</body></html>"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 <filmroellchen@serenityos.org> + * Copyright (c) 2023, Andreas Kling <kling@serenityos.org> * * SPDX-License-Identifier: BSD-2-Clause */ @@ -8,11 +9,9 @@ #include "Slide.h" #include <AK/DeprecatedString.h> -#include <AK/Forward.h> #include <AK/HashMap.h> #include <AK/NonnullOwnPtr.h> #include <AK/Vector.h> -#include <LibGfx/Painter.h> #include <LibGfx/Size.h> 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<NonnullOwnPtr<Presentation>> load_from_file(StringView file_name, NonnullRefPtr<GUI::Window> window); + static ErrorOr<NonnullOwnPtr<Presentation>> 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<DeprecatedString> render(); private: static HashMap<DeprecatedString, DeprecatedString> 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 <filmroellchen@serenityos.org> + * Copyright (c) 2023, Andreas Kling <kling@serenityos.org> * * SPDX-License-Identifier: BSD-2-Clause */ #include "PresenterWidget.h" #include "Presentation.h" -#include <AK/Format.h> #include <LibCore/MimeData.h> #include <LibFileSystemAccessClient/Client.h> #include <LibGUI/Action.h> @@ -15,13 +15,39 @@ #include <LibGUI/Menu.h> #include <LibGUI/MessageBox.h> #include <LibGUI/Painter.h> -#include <LibGUI/Window.h> -#include <LibGfx/Forward.h> -#include <LibGfx/Orientation.h> PresenterWidget::PresenterWidget() { set_min_size(100, 100); + set_fill_with_background_color(true); + m_web_view = add<WebView::OutOfProcessWebView>(); + 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>(); + float widget_ratio = static_cast<float>(event.size().width()) / static_cast<float>(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<int>(ceilf(static_cast<float>(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<int>(ceilf(static_cast<float>(event.size().width()) * hw_ratio))); + } + m_web_view->set_relative_rect(rect.centered_within(this->rect())); } ErrorOr<void> PresenterWidget::initialize_menubar() @@ -44,15 +70,13 @@ ErrorOr<void> 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<void> 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 <filmroellchen@serenityos.org> + * Copyright (c) 2023, Andreas Kling <kling@serenityos.org> * * SPDX-License-Identifier: BSD-2-Clause */ @@ -11,6 +12,7 @@ #include <LibGUI/Event.h> #include <LibGUI/UIDimensions.h> #include <LibGUI/Widget.h> +#include <LibWebView/OutOfProcessWebView.h> // 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<WebView::OutOfProcessWebView> m_web_view; + OwnPtr<Presentation> m_current_presentation; RefPtr<GUI::Action> m_next_slide_action; RefPtr<GUI::Action> 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 <filmroellchen@serenityos.org> + * Copyright (c) 2023, Andreas Kling <kling@serenityos.org> * * SPDX-License-Identifier: BSD-2-Clause */ #include "Slide.h" +#include "Presentation.h" #include <AK/JsonObject.h> -#include <AK/NonnullRefPtrVector.h> -#include <LibGUI/Window.h> -#include <LibGfx/Painter.h> -#include <LibGfx/Size.h> -#include <LibGfx/TextAlignment.h> Slide::Slide(NonnullRefPtrVector<SlideObject> slide_objects, DeprecatedString title) : m_slide_objects(move(slide_objects)) @@ -18,7 +15,7 @@ Slide::Slide(NonnullRefPtrVector<SlideObject> slide_objects, DeprecatedString ti { } -ErrorOr<Slide> Slide::parse_slide(JsonObject const& slide_json, NonnullRefPtr<GUI::Window> window) +ErrorOr<Slide> 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> Slide::parse_slide(JsonObject const& slide_json, NonnullRefPtr<GU return Error::from_string_view("Slides must be objects"sv); auto const& slide_object_json = maybe_slide_object_json.as_object(); - auto slide_object = TRY(SlideObject::parse_slide_object(slide_object_json, window)); + auto slide_object = TRY(SlideObject::parse_slide_object(slide_object_json)); slide_objects.append(move(slide_object)); } return Slide { move(slide_objects), title }; } -void Slide::paint(Gfx::Painter& painter, unsigned int current_frame, Gfx::FloatSize display_scale) const +ErrorOr<HTMLElement> 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 <filmroellchen@serenityos.org> + * Copyright (c) 2023, Andreas Kling <kling@serenityos.org> * * SPDX-License-Identifier: BSD-2-Clause */ @@ -9,19 +10,18 @@ #include "SlideObject.h" #include <AK/DeprecatedString.h> #include <AK/Forward.h> -#include <AK/NonnullOwnPtrVector.h> -#include <LibGfx/Forward.h> +#include <AK/NonnullRefPtrVector.h> // A single slide of a presentation. class Slide final { public: - static ErrorOr<Slide> parse_slide(JsonObject const& slide_json, NonnullRefPtr<GUI::Window> window); + static ErrorOr<Slide> 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<HTMLElement> render(Presentation const&) const; private: Slide(NonnullRefPtrVector<SlideObject> 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 <AK/JsonObject.h> -#include <AK/RefPtr.h> -#include <LibCore/Object.h> -#include <LibCore/Stream.h> -#include <LibGUI/Margins.h> -#include <LibGfx/Font/FontDatabase.h> -#include <LibGfx/Forward.h> -#include <LibGfx/Orientation.h> -#include <LibGfx/Painter.h> -#include <LibGfx/Size.h> -#include <LibGfx/TextWrapping.h> -#include <LibImageDecoderClient/Client.h> +#include <AK/URL.h> +#include <LibGfx/Font/FontStyleMapping.h> +#include <LibGfx/Rect.h> -ErrorOr<NonnullRefPtr<SlideObject>> SlideObject::parse_slide_object(JsonObject const& slide_object_json, NonnullRefPtr<GUI::Window> 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<float>(presentation.normative_size().width()) * 100.0f; + return DeprecatedString::formatted("{}vw", length_in_vw); +} +ErrorOr<NonnullRefPtr<SlideObject>> 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<NonnullRefPtr<SlideObject>> SlideObject::parse_slide_object(JsonObject c if (type == "text"sv) object = TRY(try_make_ref_counted<Text>()); else if (type == "image"sv) - object = TRY(try_make_ref_counted<Image>(image_decoder_client, window)); + object = TRY(try_make_ref_counted<Image>()); 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); -} - -// 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 -{ - return m_rect.to_type<float>().scaled(display_scale.width(), display_scale.height()).to_rounded<int>().translated(clip_rect.top_left()); + 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)); } -GraphicsObject::GraphicsObject() +void GraphicsObject::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 == "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)); } -Text::Text() +void Text::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 == "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)); } -void Text::paint(Gfx::Painter& painter, Gfx::FloatSize display_scale) const +void Image::set_property(StringView name, JsonValue value) { - auto scaled_bounding_box = this->transformed_bounding_box(painter.clip_rect(), display_scale); - - auto scaled_font_size = display_scale.height() * static_cast<float>(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); + 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)); } -Image::Image(NonnullRefPtr<ImageDecoderClient::Client> client, NonnullRefPtr<GUI::Window> window) - : m_client(move(client)) - , m_window(move(window)) +ErrorOr<HTMLElement> Text::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 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; } -// FIXME: Should run on another thread and report errors. -ErrorOr<void> Image::reload_image() +ErrorOr<HTMLElement> Image::render(Presentation const& presentation) 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; - return {}; + 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; } -void Image::paint(Gfx::Painter& painter, Gfx::FloatSize display_scale) const +ErrorOr<void> HTMLElement::serialize(StringBuilder& builder) 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)); + 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)); } - - 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); + 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 {}; } 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 <AK/Forward.h> -#include <AK/NonnullOwnPtr.h> -#include <AK/NonnullRefPtr.h> -#include <LibCore/Object.h> -#include <LibGUI/MessageBox.h> -#include <LibGUI/Window.h> -#include <LibGfx/Bitmap.h> #include <LibGfx/Color.h> -#include <LibGfx/Font/Font.h> -#include <LibGfx/Forward.h> -#include <LibGfx/Painter.h> +#include <LibGfx/Font/FontDatabase.h> #include <LibGfx/Rect.h> -#include <LibGfx/TextAlignment.h> -#include <LibImageDecoderClient/Client.h> -// 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); - -public: - virtual ~SlideObject() = default; - - static ErrorOr<NonnullRefPtr<SlideObject>> parse_slide_object(JsonObject const& slide_object_json, NonnullRefPtr<GUI::Window> window); +class Presentation; - // FIXME: Actually determine this from the file data. - bool is_visible_during_frame([[maybe_unused]] unsigned frame_number) const { return true; } +struct HTMLElement { + StringView tag_name; + HashMap<StringView, DeprecatedString> attributes; + HashMap<StringView, DeprecatedString> style; + DeprecatedString inner_text; + Vector<HTMLElement> children; - 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; + ErrorOr<void> serialize(StringBuilder&) const; +}; - void set_rect(Gfx::IntRect rect) { m_rect = rect; } - Gfx::IntRect rect() const { return m_rect; } +// Anything that can be on a slide. +class SlideObject : public RefCounted<SlideObject> { +public: + virtual ~SlideObject() = default; + static ErrorOr<NonnullRefPtr<SlideObject>> parse_slide_object(JsonObject const& slide_object_json); + virtual ErrorOr<HTMLElement> render(Presentation const&) const = 0; protected: - SlideObject(); + SlideObject() = default; + virtual void set_property(StringView name, JsonValue); + + HashMap<DeprecatedString, JsonValue> 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; - - 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; } +private: + virtual ErrorOr<HTMLElement> render(Presentation const&) const override; + virtual void set_property(StringView name, JsonValue) override; -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<ImageDecoderClient::Client>, NonnullRefPtr<GUI::Window>); + 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<void> reload_image(); + DeprecatedString m_src; + StringView m_image_rendering; - RefPtr<Gfx::Bitmap> m_currently_loaded_image; - NonnullRefPtr<ImageDecoderClient::Client> m_client; - NonnullRefPtr<GUI::Window> m_window; + virtual ErrorOr<HTMLElement> 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 <filmroellchen@serenityos.org> + * Copyright (c) 2023, Andreas Kling <kling@serenityos.org> * * SPDX-License-Identifier: BSD-2-Clause */ @@ -14,7 +15,7 @@ ErrorOr<int> 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; |