diff --git a/Meta/CMake/code_generators.cmake b/Meta/CMake/code_generators.cmake index c7d9b60948..59ec3f9b79 100644 --- a/Meta/CMake/code_generators.cmake +++ b/Meta/CMake/code_generators.cmake @@ -18,6 +18,22 @@ function(stringify_gml source output string_name) add_dependencies(all_generated generate_${output_name}) endfunction() +function(compile_gml source output) + set(source ${CMAKE_CURRENT_SOURCE_DIR}/${source}) + add_custom_command( + OUTPUT ${output} + COMMAND $ ${source} > ${output}.tmp + COMMAND "${CMAKE_COMMAND}" -E copy_if_different ${output}.tmp ${output} + COMMAND "${CMAKE_COMMAND}" -E remove ${output}.tmp + VERBATIM + DEPENDS Lagom::GMLCompiler + MAIN_DEPENDENCY ${source} + ) + get_filename_component(output_name ${output} NAME) + add_custom_target(generate_${output_name} DEPENDS ${output}) + add_dependencies(all_generated generate_${output_name}) +endfunction() + function(compile_ipc source output) if (NOT IS_ABSOLUTE ${source}) set(source ${CMAKE_CURRENT_SOURCE_DIR}/${source}) diff --git a/Meta/Lagom/CMakeLists.txt b/Meta/Lagom/CMakeLists.txt index dc8284ee65..6762b038d4 100644 --- a/Meta/Lagom/CMakeLists.txt +++ b/Meta/Lagom/CMakeLists.txt @@ -370,6 +370,17 @@ install(TARGETS LibTimeZone EXPORT LagomTargets) # This is used by the BindingsGenerator so needs to always be built. add_serenity_subdirectory(Userland/Libraries/LibIDL) +# LibGUI - only GML +# This is used by the GML compiler and therefore always needed. +set(LIBGUI_GML_SOURCES + GML/Lexer.cpp + GML/Parser.cpp +) +list(TRANSFORM LIBGUI_GML_SOURCES PREPEND "${SERENITY_PROJECT_ROOT}/Userland/Libraries/LibGUI/") +lagom_lib(LibGUI_GML gui_gml + SOURCES ${LIBGUI_GML_SOURCES} +) + # Manually install AK headers install( DIRECTORY "${SERENITY_PROJECT_ROOT}/AK" diff --git a/Meta/Lagom/Tools/CodeGenerators/CMakeLists.txt b/Meta/Lagom/Tools/CodeGenerators/CMakeLists.txt index c391d6e8b7..fff1d4276b 100644 --- a/Meta/Lagom/Tools/CodeGenerators/CMakeLists.txt +++ b/Meta/Lagom/Tools/CodeGenerators/CMakeLists.txt @@ -1,3 +1,4 @@ +add_subdirectory(GMLCompiler) add_subdirectory(IPCCompiler) add_subdirectory(LibEDID) add_subdirectory(LibGL) diff --git a/Meta/Lagom/Tools/CodeGenerators/GMLCompiler/CMakeLists.txt b/Meta/Lagom/Tools/CodeGenerators/GMLCompiler/CMakeLists.txt new file mode 100644 index 0000000000..ad30e72e05 --- /dev/null +++ b/Meta/Lagom/Tools/CodeGenerators/GMLCompiler/CMakeLists.txt @@ -0,0 +1,5 @@ +set(SOURCES + main.cpp +) + +lagom_tool(GMLCompiler LIBS LibMain LibCore LibGUI_GML) diff --git a/Meta/Lagom/Tools/CodeGenerators/GMLCompiler/main.cpp b/Meta/Lagom/Tools/CodeGenerators/GMLCompiler/main.cpp new file mode 100644 index 0000000000..ee37eecf45 --- /dev/null +++ b/Meta/Lagom/Tools/CodeGenerators/GMLCompiler/main.cpp @@ -0,0 +1,301 @@ +/* + * Copyright (c) 2023, kleines Filmröllchen + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +enum class UseObjectConstructor : bool { + No, + Yes, +}; + +// Classes whose header doesn't have the same name as the class. +static Optional map_class_to_file(StringView class_) +{ + static HashMap class_file_mappings { + { "GUI::HorizontalSplitter"sv, "GUI/Splitter"sv }, + { "GUI::VerticalSplitter"sv, "GUI/Splitter"sv }, + { "GUI::HorizontalBoxLayout"sv, "GUI/BoxLayout"sv }, + { "GUI::VerticalBoxLayout"sv, "GUI/BoxLayout"sv }, + { "GUI::HorizontalProgressbar"sv, "GUI/Progressbar"sv }, + { "GUI::VerticalProgressbar"sv, "GUI/Progressbar"sv }, + }; + return class_file_mappings.get(class_); +} + +// Properties which don't take a direct JSON-like primitive (StringView, int, bool, Array etc) as arguments and need the arguments to be wrapped in a constructor call. +static Optional map_property_to_type(StringView property) +{ + static HashMap property_to_type_mappings { + { "container_margins"sv, "GUI::Margins"sv }, + { "margins"sv, "GUI::Margins"sv }, + }; + return property_to_type_mappings.get(property); +} + +// Properties which take a UIDimension which can handle JSON directly. +static bool is_ui_dimension_property(StringView property) +{ + static HashTable ui_dimension_properties; + if (ui_dimension_properties.is_empty()) { + ui_dimension_properties.set("min_width"sv); + ui_dimension_properties.set("max_width"sv); + ui_dimension_properties.set("preferred_width"sv); + ui_dimension_properties.set("min_height"sv); + ui_dimension_properties.set("max_height"sv); + ui_dimension_properties.set("preferred_height"sv); + } + return ui_dimension_properties.contains(property); +} + +// FIXME: Since normal string-based properties take either String or StringView (and the latter can be implicitly constructed from the former), +// we need to special-case DeprecatedString property setters while those still exist. +// Please remove a setter from this list once it uses StringView or String. +static bool takes_deprecated_string(StringView property) +{ + static HashTable deprecated_string_properties; + if (deprecated_string_properties.is_empty()) { + deprecated_string_properties.set("name"sv); + } + return deprecated_string_properties.contains(property); +} + +static ErrorOr include_path_for(StringView class_name) +{ + String pathed_name; + if (auto mapping = map_class_to_file(class_name); mapping.has_value()) + pathed_name = TRY(String::from_utf8(mapping.value())); + else + pathed_name = TRY(TRY(String::from_utf8(class_name)).replace("::"sv, "/"sv, ReplaceMode::All)); + + if (class_name.starts_with("GUI::"sv) || class_name.starts_with("WebView::"sv)) + return String::formatted("", pathed_name); + + // We assume that all other paths are within the current application, for now. + return String::formatted("", pathed_name); +} + +// Each entry is an include path, without the "#include" itself. +static ErrorOr> extract_necessary_includes(GUI::GML::Object const& gml_hierarchy) +{ + HashTable necessary_includes; + TRY(necessary_includes.try_set(TRY(include_path_for(gml_hierarchy.name())))); + if (gml_hierarchy.layout_object() != nullptr) + TRY(necessary_includes.try_set(TRY(include_path_for(gml_hierarchy.layout_object()->name())))); + + TRY(gml_hierarchy.try_for_each_child_object([&](auto const& object) -> ErrorOr { + auto necessary_child_includes = TRY(extract_necessary_includes(object)); + for (auto const& include : necessary_child_includes) + TRY(necessary_includes.try_set(include)); + return {}; + })); + + return necessary_includes; +} + +static char const header[] = R"~~~( +/* + * Auto-generated by the GML compiler + */ + +)~~~"; + +static char const function_start[] = R"~~~( +// Creates a @main_class_name@ and initializes it. +// This function was auto-generated by the GML compiler. +ErrorOr> @main_class_name@::try_create() +{ + RefPtr<@main_class_name@> main_object; + +)~~~"; + +static char const footer[] = R"~~~( + return main_object.release_nonnull(); +} +)~~~"; + +// FIXME: In case of error, propagate the precise array+property that triggered the error. +static ErrorOr generate_initializer_for(Optional property_name, JsonValue value) +{ + if (value.is_string()) { + if (property_name.has_value() && takes_deprecated_string(*property_name)) + return String::formatted(R"~~~("{}"sv)~~~", value.as_string()); + return String::formatted(R"~~~("{}"_string)~~~", value.as_string()); + } + // No need to handle the smaller integer types separately. + if (value.is_integer()) + return String::formatted("static_cast({})", value.as_integer()); + if (value.is_integer()) + return String::formatted("static_cast({})", value.as_integer()); + if (value.is_bool()) + return String::formatted("{}", value.as_bool()); + if (value.is_double()) + return String::formatted("static_cast({})", value.as_double()); + if (value.is_array()) { + auto const& array = value.as_array(); + auto child_type = Optional {}; + for (auto const& child_value : array.values()) { + if (child_value.is_array()) + return Error::from_string_view("Nested arrays are not supported"sv); + +#define HANDLE_TYPE(type_name, is_type) \ + if (child_value.is_type() && (!child_type.has_value() || child_type.value() == #type_name##sv)) \ + child_type = #type_name##sv; \ + else + + HANDLE_TYPE(StringView, is_string) + HANDLE_TYPE(i64, is_integer) + HANDLE_TYPE(u64, is_integer) + HANDLE_TYPE(bool, is_bool) + HANDLE_TYPE(double, is_double) + return Error::from_string_view("Inconsistent contained type in JSON array"sv); +#undef HANDLE_TYPE + } + if (!child_type.has_value()) + return Error::from_string_view("Empty JSON array; cannot deduce type."sv); + + StringBuilder initializer; + initializer.appendff("Array<{}, {}> {{ "sv, child_type.release_value(), array.size()); + for (auto const& child_value : array.values()) + initializer.appendff("{}, ", TRY(generate_initializer_for({}, child_value))); + initializer.append("}"sv); + return initializer.to_string(); + } + return Error::from_string_view("Unsupported JSON value"sv); +} + +// Loads an object and assigns it to the RefPtr variable named object_name. +// All loading happens in a separate block. +static ErrorOr generate_loader_for_object(GUI::GML::Object const& gml_object, SourceGenerator generator, String object_name, size_t indentation, UseObjectConstructor use_object_constructor) +{ + generator.set("object_name", object_name.to_deprecated_string()); + generator.set("class_name", gml_object.name()); + + auto append = [&](auto& generator, char const(&text)[N]) -> ErrorOr { + generator.append(TRY(String::repeated(' ', indentation * 4)).bytes_as_string_view()); + generator.appendln(text); + return {}; + }; + + generator.append(TRY(String::repeated(' ', (indentation - 1) * 4)).bytes_as_string_view()); + generator.appendln("{"); + if (use_object_constructor == UseObjectConstructor::Yes) + TRY(append(generator, "@object_name@ = TRY(@class_name@::try_create());")); + else + TRY(append(generator, "@object_name@ = TRY(adopt_nonnull_ref_or_enomem(new (nothrow) @class_name@()));")); + + // Properties + TRY(gml_object.try_for_each_property([&](StringView key, NonnullRefPtr value) -> ErrorOr { + auto value_code = TRY(generate_initializer_for(key, value)); + if (is_ui_dimension_property(key)) { + if (auto ui_dimension = GUI::UIDimension::construct_from_json_value(value); ui_dimension.has_value()) + value_code = TRY(ui_dimension->as_cpp_source()); + else + // FIXME: propagate precise error cause + return Error::from_string_view("UI dimension invalid"sv); + } else { + // Wrap value in an extra constructor call if necessary. + if (auto type = map_property_to_type(key); type.has_value()) + value_code = TRY(String::formatted("{} {{ {} }}", type.release_value(), value_code)); + } + + auto property_generator = TRY(generator.fork()); + property_generator.set("key", key); + property_generator.set("value", value_code.bytes_as_string_view()); + TRY(append(property_generator, R"~~~(@object_name@->set_@key@(@value@);)~~~")); + return {}; + })); + generator.appendln(""); + + // Layout + if (gml_object.layout_object() != nullptr) { + TRY(append(generator, "RefPtr layout;")); + TRY(generate_loader_for_object(*gml_object.layout_object(), TRY(generator.fork()), TRY(String::from_utf8("layout"sv)), indentation + 1, UseObjectConstructor::Yes)); + TRY(append(generator, "@object_name@->set_layout(layout.release_nonnull());")); + generator.appendln(""); + } + + // Children + size_t current_child_index = 0; + auto next_child_name = [&]() { + return String::formatted("{}_child_{}", object_name, current_child_index++); + }; + TRY(gml_object.try_for_each_child_object([&](auto const& child) -> ErrorOr { + auto child_generator = TRY(generator.fork()); + auto child_variable_name = TRY(next_child_name()); + child_generator.set("child_variable_name", child_variable_name.bytes_as_string_view()); + child_generator.set("child_class_name", child.name()); + TRY(append(child_generator, "RefPtr<@child_class_name@> @child_variable_name@;")); + TRY(generate_loader_for_object(child, TRY(child_generator.fork()), child_variable_name, indentation + 1, UseObjectConstructor::Yes)); + + // Handle the current two special cases of child adding. + if (gml_object.name() == "GUI::ScrollableContainerWidget"sv) + TRY(append(child_generator, "static_ptr_cast(@object_name@)->set_widget(*@child_variable_name@);")); + else if (gml_object.name() == "GUI::TabWidget"sv) + TRY(append(child_generator, "static_ptr_cast(@object_name@)->add_widget(*@child_variable_name@);")); + else + TRY(append(child_generator, "TRY(@object_name@->try_add_child(*@child_variable_name@));")); + child_generator.appendln(""); + return {}; + })); + + generator.append(TRY(String::repeated(' ', (indentation - 1) * 4)).bytes_as_string_view()); + generator.appendln("}"); + + return {}; +} + +static ErrorOr generate_cpp(NonnullRefPtr gml) +{ + StringBuilder builder; + SourceGenerator generator { builder }; + + generator.append(header); + + auto& main_class = gml->main_class(); + auto necessary_includes = TRY(extract_necessary_includes(main_class)); + static String const always_necessary_includes[] = { + TRY(String::from_utf8(""sv)), + TRY(String::from_utf8(""sv)), + TRY(String::from_utf8(""sv)), + TRY(String::from_utf8(""sv)), + TRY(String::from_utf8(""sv)), + }; + TRY(necessary_includes.try_set_from(always_necessary_includes)); + for (auto const& include : necessary_includes) + generator.appendln(TRY(String::formatted("#include {}", include)).bytes_as_string_view()); + + // FIXME: Use a UTF-8 aware function once possible. + generator.set("main_class_name", main_class.name()); + generator.append(function_start); + TRY(generate_loader_for_object(main_class, TRY(generator.fork()), "main_object"_string, 2, UseObjectConstructor::No)); + + generator.append(footer); + return builder.to_string(); +} + +ErrorOr serenity_main(Main::Arguments arguments) +{ + Core::ArgsParser argument_parser; + StringView gml_file_name; + argument_parser.add_positional_argument(gml_file_name, "GML file to compile", "GML_FILE", Core::ArgsParser::Required::Yes); + argument_parser.parse(arguments); + + auto gml_text = TRY(TRY(Core::File::open(gml_file_name, Core::File::OpenMode::Read))->read_until_eof()); + auto parsed_gml = TRY(GUI::GML::parse_gml(gml_text)); + auto generated_cpp = TRY(generate_cpp(parsed_gml)); + outln("{}", generated_cpp); + return 0; +} diff --git a/Userland/Libraries/LibGUI/GML/AST.h b/Userland/Libraries/LibGUI/GML/AST.h index 039002c144..7e62947116 100644 --- a/Userland/Libraries/LibGUI/GML/AST.h +++ b/Userland/Libraries/LibGUI/GML/AST.h @@ -188,6 +188,19 @@ public: } } + template> Callback> + ErrorOr try_for_each_property(Callback callback) const + { + for (auto const& child : m_properties) { + if (is(child)) { + auto const& property = static_cast(*child); + if (property.key() != "layout" && is(property.value().ptr())) + TRY(callback(property.key(), static_ptr_cast(property.value()))); + } + } + return {}; + } + template void for_each_child_object(Callback callback) const {