From 18408d19ef0f4c81ec87155da458a776ba2705c2 Mon Sep 17 00:00:00 2001 From: Clem Cords Date: Thu, 16 Nov 2023 19:36:43 +0100 Subject: [PATCH 01/31] added set_menubar, native pointers --- docs/src/01_manual/08_menus.md | 23 +++++++++++++- docs/src/01_manual/10_theme_customization.md | 2 +- src/Mousetrap.jl | 21 +++++++++++++ src/docgen/functions.jl | 33 ++++++++++++++++++++ test/runtests.jl | 15 +++++++++ 5 files changed, 92 insertions(+), 2 deletions(-) diff --git a/docs/src/01_manual/08_menus.md b/docs/src/01_manual/08_menus.md index 445524f..c246a4e 100644 --- a/docs/src/01_manual/08_menus.md +++ b/docs/src/01_manual/08_menus.md @@ -260,7 +260,7 @@ The **top-level** menu is `root`. It is used as the argument for the constructor No direct child of `root` is an "action"-, "widget"-, "icon"- or "section"-type item. This is what is required for `MenuBar`. All top-level items have to be submenus. -!!! Warning +!!! warning Due to a bug in the backend, as of `v0.3.0`, a menu model used for a `MenuBar` **may not have a "widget"-type item in a submenu of a submenu**. This means we *can* add a widget to any submenu of `root`, but we may not add @@ -269,6 +269,27 @@ No direct child of `root` is an "action"-, "widget"-, "icon"- or "section"-type This bug does not affect `PopoverMenu`, for whom we can put a widget at any depth. `PopoverMenu` has no requirement as to the structure of its menu model, while `MenuBar` requires that all top-level items are submenus and that no submenu of a submenu may have a "widget"-type item. +## Main Menu (macOS only) + +!!! compat + Features from this section are only available with Mousetrap `v0.3.1` or newer, and should only be used by applications targeting macOS. We can use `Sys.isapple` to verify the user's operating system. + +On macOS, applications are able to target the [**main menu**](https://support.apple.com/en-gb/guide/mac-help/mchlp1446/mac), which is the menubar at the very top of the screen (outside the Mousetrap window). This bar contains the Apple menu, as well as a native menubar with application-specific options. To bind a `Mousetrap.MenuModel` to this menubar, we use `set_menubar` on our `Application` instance: + +```julia +main() do app::Application + + model = MenuModel() + # fill model here + + if Sys.isapple() + set_menubar(app, model) # associate model with the Apple main menu + end +end +``` + +Note that it may be necessary to call `set_menubar` again when the `MenuModel` is modified in order for the main menu to be updated. + --- ## Style End Note diff --git a/docs/src/01_manual/10_theme_customization.md b/docs/src/01_manual/10_theme_customization.md index 672053f..6c22519 100644 --- a/docs/src/01_manual/10_theme_customization.md +++ b/docs/src/01_manual/10_theme_customization.md @@ -198,7 +198,7 @@ By default, the function used to map the elapsed duration of the `Animation` to Mousetrap uses [Cascading Style Sheets (CSS)](https://developer.mozilla.org/en-US/docs/Web/CSS) to define the exact look of a widget. A UI theme is nothing more than a huge CSS file from which all widgets take information about how they should be rendered. -!!! Warning "CSS" +!!! warning "CSS" The rest of this chapter will assume that readers are familiar with the basics of CSS. Readers are encouraged to consult [this documentation](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference) for CSS-related questions. ## Applying CSS Properties to a Widget diff --git a/src/Mousetrap.jl b/src/Mousetrap.jl index ec8b7ab..dc4cb4f 100644 --- a/src/Mousetrap.jl +++ b/src/Mousetrap.jl @@ -3211,6 +3211,13 @@ module Mousetrap @add_signal_items_changed MenuModel + function set_menubar(app::Application, model::MenuModel) + if !Sys.isapple() + log_warning(MOUSETRAP_DOMAIN, "In set_menubar: setting an application-wide menubar is only supported on macOS. Use `Sys.isapple()` to verify the users system before calling this function") + end + Mousetrap.detail.set_menubar(app._internal, model._internal) + end + Base.show(io::IO, x::MenuModel) = show_aux(io, x) ###### menubar.jl @@ -5573,4 +5580,18 @@ end # else MOUSETRAP_ENABLE_OPENGL_COMPONENT return out end @define_compound_widget_signals() + +###### internal.jl + + function as_gobject_pointer(x::T) where T <: Union{SignalEmitter, Widget} :: Ptr{Cvoid} + return Mousetrap.detail.as_gobject(x._internal.cpp_object) + end + + function as_internal_pointer(x::T) where T <: Union{SignalEmitter, Widget} ::Ptr{Cvoid} + return Mousetrap.detail.as_internal(x._internal.cpp_object) + end + + function as_native_widget(x::Widget) + return Mousetrap.detail.as_native_widget(Mousetrap.as_widget_pointer(x)) + end end \ No newline at end of file diff --git a/src/docgen/functions.jl b/src/docgen/functions.jl index 56bff0c..01bf9ce 100644 --- a/src/docgen/functions.jl +++ b/src/docgen/functions.jl @@ -427,6 +427,22 @@ Create a new image that is a horizontally and/or vertically mirrored. This function does not modify the original image. """ +@document as_gobject_pointer """ +``` +as_gobject_pointer(<: SignalEmitter) -> Ptr{Cvoid} +as_gobject_pointer(<: Widget) -> Ptr{Cvoid} +``` +Get the raw C pointer pointing to the native `GObject` in memory, for use with `ccall` and `Glib_jll`. Note that Mousetrap handles the lifetime of this object automatically, calling `g_object_ref` or `g_object_unref` on the pointer will cause memory leaks or segmentation faults. +""" + +@document as_internal_pointer """ +``` +as_internal_pointer(<: SignalEmitter) -> Ptr{Cvoid} +as_internal_pointer(<: Widget) -> Ptr{Cvoid} +``` +Get the raw C pointer pointing to the Mousetrap C++ object, for use with `ccall` and `mousetrap_jll.mousetrap`. +""" + @document as_line! """ ``` as_line!(::Shape, a::Vector2f, b::Vector2f) @@ -448,6 +464,13 @@ as_lines!(::Shape, points::Vector{Pair{Vector2f, Vector2f}}) Create a shape as a set of unconnected lines, vertex positions in OpenGL coordinates. """ +@document as_native_widget """ +``` +as_native_widget(<: Widget) -> Ptr{Cvoid} +``` +Get the raw C pointer pointing to the native `GObject` in memory, for use with `ccall` and `GTK4_jll`. Note that Mousetrap handles the lifetime of this object automatically, calling `g_object_ref` or `g_object_unref` on the pointer will cause memory leaks or segmentation faults. +""" + @document as_microseconds """ ``` as_microseconds(time::Time) -> Float64 @@ -4690,6 +4713,16 @@ set_message!(::AlertDialog, ::String) Set the main message of the dialog, this will be used as the dialogs title. """ +@document set_menubar """ +``` +set_menubar(::Application, ::MenuModel) +``` +Associate a menu model with the application-wide menubar. On macOS only, this will update the Apple main menu at the very top of the screen. + +This function will print a warning if called on a non-Apple machine. Use `Sys.isapple` to verify the users OS before calling this function. +``` +""" + @document set_minimized! """ ``` set_minimized!(::Window, ::Bool) diff --git a/test/runtests.jl b/test/runtests.jl index d43824f..1d28607 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1291,6 +1291,16 @@ function test_image_display(::Container) end end +### INTERNAL + +function test_internal(x::Container) + @testset "Internal" begin + @test as_gobject_pointer(x) != C_NULL + @test as_internal_pointer(x) != C_NULL + @test as_native_widget(x) != C_NULL + end +end + ### KEY_FILE function test_key_file(::Container) @@ -1565,6 +1575,10 @@ function test_menus(::Container) @testset "MenuModel" begin Base.show(devnull, root) @test items_changed_called[] == true + + if Sys.isapple() + set_menubar(app[], root) + end end @testset "MenuBar" begin @@ -2822,6 +2836,7 @@ main(Main.app_id) do app::Application test_icon(container) test_image(container) test_image_display(container) + test_internal(container) test_key_file(container) test_label(container) test_level_bar(container) From d80a8f8ba8c4ca93f7415dd0de787c6fbab5ae31 Mon Sep 17 00:00:00 2001 From: Clem Cords Date: Mon, 11 Dec 2023 16:20:55 +0100 Subject: [PATCH 02/31] fixed row_i index --- src/Mousetrap.jl | 2 +- test/example.jl | 34 +++++++++++++++++++++++++++++++++- test/runtests.jl | 28 ++++++++++++++++++++++++++++ 3 files changed, 62 insertions(+), 2 deletions(-) diff --git a/src/Mousetrap.jl b/src/Mousetrap.jl index dc4cb4f..19e56b9 100644 --- a/src/Mousetrap.jl +++ b/src/Mousetrap.jl @@ -4132,7 +4132,7 @@ module Mousetrap @log_warning MOUSETRAP_DOMAIN "In ColumnView::push_back_rows: Attempting to push $(length(widgets)) widgets, but ColumnView only has $(get_n_columns(column_view)) columns" end - row_i = get_n_rows(column_view) + row_i = get_n_rows(column_view) + 1 for i in 1:get_n_columns(column_view) column = get_column_at(column_view, i) set_widget_at!(column_view, column, row_i, widgets[i]) diff --git a/test/example.jl b/test/example.jl index 2ebb4e9..eb9ed54 100644 --- a/test/example.jl +++ b/test/example.jl @@ -1,6 +1,36 @@ # File used for debugging and for dosc examples, why are you snooping through this file? using Mousetrap +main() do app::Application + window = Window(app) + + column_view = ColumnView() + + row_index = push_back_column!(column_view, " ") + count_column = push_back_column!(column_view, "#") + name_column = push_back_column!(column_view, "Name") + weigt_column = push_back_column!(column_view, "Weight") + unit_column = push_back_column!(column_view, "Units") + + # fill columns with example text + for i in 1:100 + push_back_row!(column_view, + Label(string(i)), # row index + Label(string(rand(0:99))), # count + Label(rand(["Apple", "Orange", "Banana", "Kumquat", "Durian", "Mangosteen"])), # name + Label(string(rand(0:100))), # weight + Label(string(rand(["mg", "g", "kg", "ton"]))) # unit + ) + end + + scrolled_viewport = Viewport() + set_child!(scrolled_viewport, column_view) + set_child!(window, scrolled_viewport) + present!(window) +end + +if false + add_css!(""" @keyframes spin-animation { 0% { transform: rotate(0turn) scale(1); } @@ -657,4 +687,6 @@ end end # @static if - =# \ No newline at end of file + =# + +end \ No newline at end of file diff --git a/test/runtests.jl b/test/runtests.jl index 1d28607..81a71f0 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -2874,3 +2874,31 @@ main(Main.app_id) do app::Application close!(window) #quit!(app) end + +using Mousetrap +main() do app + window = Window(app) + + revealer = Revealer() + set_transition_type!(revealer, REVEALER_TRANSITION_TYPE_SLIDE_LEFT) + + button = Button() + set_child!(button, Label("click me")) + connect_signal_clicked!(button) do self + set_is_revealed!(revealer, !get_is_revealed(revealer)) + end + + to_reveal = Label("REVEALED") + set_margin!(to_reveal, 10) + set_child!(revealer, to_reveal) + set_is_revealed!(revealer, false) + connect_signal_revealed!(revealer) do self + println("revealed") + end + + box = Box(ORIENTATION_HORIZONTAL) + push_back!(box, button) + push_back!(box, revealer) + set_child!(window, box) + present!(window) +end From 555f979aab887882eaa8b4023072bc6517d07759 Mon Sep 17 00:00:00 2001 From: Clem Cords Date: Mon, 11 Dec 2023 18:44:01 +0100 Subject: [PATCH 03/31] native pointers working --- src/Mousetrap.jl | 19 +++++++++++++++++-- src/docgen/functions.jl | 14 ++++++++++++++ test/example.jl | 20 +++++++++++++++----- test/runtests.jl | 10 +++++++--- 4 files changed, 53 insertions(+), 10 deletions(-) diff --git a/src/Mousetrap.jl b/src/Mousetrap.jl index 19e56b9..31b520a 100644 --- a/src/Mousetrap.jl +++ b/src/Mousetrap.jl @@ -4085,6 +4085,17 @@ module Mousetrap @export_function ColumnViewColumn set_is_resizable! Cvoid Bool b @export_function ColumnViewColumn get_is_resizable Bool + function set_expand!(column::ColumnViewColumn, should_expand::Bool) + @ccall detail.GTK4_jll.libgtk4.gtk_column_view_column_set_expand(Mousetrap.as_internal_pointer(column)::Ptr{Cvoid}, should_expand::Bool)::Cvoid + return nothing + end + export set_expand! + + function get_expand(column::ColumnViewColumn) ::Bool + return @ccall detail.GTK4_jll.libgtk4.gtk_column_view_column_get_expand(Mousetrap.as_internal_pointer(column)::Ptr{Cvoid})::Bool + end + export get_expand + @export_type ColumnView Widget @declare_native_widget ColumnView @@ -5583,14 +5594,18 @@ end # else MOUSETRAP_ENABLE_OPENGL_COMPONENT ###### internal.jl - function as_gobject_pointer(x::T) where T <: Union{SignalEmitter, Widget} :: Ptr{Cvoid} + function as_gobject_pointer(x::T) where T <: Union{SignalEmitter, Widget} #::Ptr{Cvoid} return Mousetrap.detail.as_gobject(x._internal.cpp_object) end - function as_internal_pointer(x::T) where T <: Union{SignalEmitter, Widget} ::Ptr{Cvoid} + function as_internal_pointer(x::T) where T <: Union{SignalEmitter, Widget} #::Ptr{Cvoid} return Mousetrap.detail.as_internal(x._internal.cpp_object) end + function as_internal_pointer(column::ColumnViewColumn) + return @ccall detail.mousetrap_jll.mousetrap._ZNK9mousetrap10ColumnView6Column12get_internalEv(column._internal.cpp_object::Ptr{Cvoid})::Ptr{Cvoid} + end + function as_native_widget(x::Widget) return Mousetrap.detail.as_native_widget(Mousetrap.as_widget_pointer(x)) end diff --git a/src/docgen/functions.jl b/src/docgen/functions.jl index 01bf9ce..2d4f64b 100644 --- a/src/docgen/functions.jl +++ b/src/docgen/functions.jl @@ -1219,6 +1219,13 @@ get_end_child_shrinkable(::Paned) -> Bool Get whether the user can resize the end child such that its allocated area inside the `Paned` is smaller than the natural size of the child. """ +@document get_expand """ +``` +get_expand(::ColumnViewColumn) -> Bool +``` +Get whether this column expands horizontally. +""" + @document get_expand_horizontally """ ``` get_expand_horizontally(::Widget) -> Bool @@ -4179,6 +4186,13 @@ Set whether the user can resize the end child such that its allocated area insid set_expand!(::Widget, ::Bool) ``` Set whether the widget should expand along both the horizontal and vertical axis. + +--- + +``` +set_expand!(::ColumnViewColumn, ::Bool) +``` +Set whether the column should expand along the horizontal axis. """ @document set_is_expanded! """ diff --git a/test/example.jl b/test/example.jl index eb9ed54..3771867 100644 --- a/test/example.jl +++ b/test/example.jl @@ -4,27 +4,37 @@ using Mousetrap main() do app::Application window = Window(app) + # create column view with columns column_view = ColumnView() - row_index = push_back_column!(column_view, " ") + row_index_column = push_back_column!(column_view, " ") count_column = push_back_column!(column_view, "#") name_column = push_back_column!(column_view, "Name") - weigt_column = push_back_column!(column_view, "Weight") + weight_column = push_back_column!(column_view, "Weight") unit_column = push_back_column!(column_view, "Units") - # fill columns with example text + set_expand!.((row_index, count_column, name_column, weight_column, unit_column), true) + + # fill columns with text for i in 1:100 - push_back_row!(column_view, + row = [ Label(string(i)), # row index Label(string(rand(0:99))), # count Label(rand(["Apple", "Orange", "Banana", "Kumquat", "Durian", "Mangosteen"])), # name Label(string(rand(0:100))), # weight Label(string(rand(["mg", "g", "kg", "ton"]))) # unit - ) + ] + + set_horizontal_alignment!.(row, ALIGNMENT_START) + push_back_row!(column_view, row...) end + # create viewport, this will add a scrollbar scrolled_viewport = Viewport() + set_propagate_natural_width!(scrolled_viewport, true) # hides horizontal scrollbar set_child!(scrolled_viewport, column_view) + + # show both in window set_child!(window, scrolled_viewport) present!(window) end diff --git a/test/runtests.jl b/test/runtests.jl index 81a71f0..edf518e 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -749,6 +749,10 @@ function test_column_view(::Container) column_name = "column 02" column = insert_column_at!(column_view, 1, column_name) + @test get_expand(column) == false + set_expand!(column, true) + @test get_expand(column) == true + @test get_title(column) == column_name new_title = "new title" @@ -1295,9 +1299,9 @@ end function test_internal(x::Container) @testset "Internal" begin - @test as_gobject_pointer(x) != C_NULL - @test as_internal_pointer(x) != C_NULL - @test as_native_widget(x) != C_NULL + @test Mousetrap.as_gobject_pointer(x) != C_NULL + @test Mousetrap.as_internal_pointer(x) != C_NULL + @test Mousetrap.as_native_widget(x) != C_NULL end end From d15ef9ffe1f79481cb5966f53f103463f3242ecf Mon Sep 17 00:00:00 2001 From: Clem Cords Date: Mon, 11 Dec 2023 18:45:41 +0100 Subject: [PATCH 04/31] removed debug example --- test/runtests.jl | 28 ---------------------------- 1 file changed, 28 deletions(-) diff --git a/test/runtests.jl b/test/runtests.jl index edf518e..2ae7c4c 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -2878,31 +2878,3 @@ main(Main.app_id) do app::Application close!(window) #quit!(app) end - -using Mousetrap -main() do app - window = Window(app) - - revealer = Revealer() - set_transition_type!(revealer, REVEALER_TRANSITION_TYPE_SLIDE_LEFT) - - button = Button() - set_child!(button, Label("click me")) - connect_signal_clicked!(button) do self - set_is_revealed!(revealer, !get_is_revealed(revealer)) - end - - to_reveal = Label("REVEALED") - set_margin!(to_reveal, 10) - set_child!(revealer, to_reveal) - set_is_revealed!(revealer, false) - connect_signal_revealed!(revealer) do self - println("revealed") - end - - box = Box(ORIENTATION_HORIZONTAL) - push_back!(box, button) - push_back!(box, revealer) - set_child!(window, box) - present!(window) -end From 40e264da4fa36887d1f31f4940276a94ed7a0c1a Mon Sep 17 00:00:00 2001 From: Clem Cords Date: Thu, 8 Feb 2024 16:57:26 +0100 Subject: [PATCH 05/31] tests passing --- src/Mousetrap.jl | 4 ++-- test/runtests.jl | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Mousetrap.jl b/src/Mousetrap.jl index 31b520a..ddd48af 100644 --- a/src/Mousetrap.jl +++ b/src/Mousetrap.jl @@ -4160,7 +4160,7 @@ module Mousetrap row_i = 1 for i in 1:get_n_columns(column_view) column = get_column_at(column_view, i) - set_widget_at!(column_view, column, from_julia_index(row_i), widgets[i]) + set_widget_at!(column_view, column, row_i, widgets[i]) end end export push_front_row! @@ -4174,7 +4174,7 @@ module Mousetrap row_i = index for i in 1:get_n_columns(column_view) column = get_column_at(column_view, i) - set_widget!(column_view, column, row_i, widgets[i]) + set_widget_at!(column_view, column, row_i, widgets[i]) end end export push_front_row! diff --git a/test/runtests.jl b/test/runtests.jl index 2ae7c4c..0fada3b 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -753,6 +753,10 @@ function test_column_view(::Container) set_expand!(column, true) @test get_expand(column) == true + push_back_row!(column_view, Label(""), Label(""), Label("")) + push_front_row!(column_view, Label(""), Label(""), Label("")) + insert_row_at!(column_view, 2, Label(""), Label(""), Label("")) + @test get_title(column) == column_name new_title = "new title" From 6a7a882d83774932a41784f4e8545e5c83252e7d Mon Sep 17 00:00:00 2001 From: Clem Cords Date: Thu, 8 Feb 2024 18:03:18 +0100 Subject: [PATCH 06/31] stashing before fixing no signal error message --- src/Mousetrap.jl | 7 +- src/docgen/signals.jl | 2 - test/example.jl | 16 +++- test/runtests.jl | 167 ++++++++---------------------------------- 4 files changed, 45 insertions(+), 147 deletions(-) diff --git a/src/Mousetrap.jl b/src/Mousetrap.jl index 6c2e27f..e2708b1 100644 --- a/src/Mousetrap.jl +++ b/src/Mousetrap.jl @@ -376,11 +376,6 @@ module Mousetrap push!(out.args, esc( :(is_native_widget(::$Type) = return true) )) - #= - push!(out.args, esc( - :(Mousetrap.get_top_level_widget(x::$Type) = return x) - )) - =# return out end @@ -4162,7 +4157,7 @@ module Mousetrap row_i = 1 for i in 1:get_n_columns(column_view) column = get_column_at(column_view, i) - set_widget_at!(column_view, column, row_i, widgets[i] + set_widget_at!(column_view, column, row_i, widgets[i]) end end export push_front_row! diff --git a/src/docgen/signals.jl b/src/docgen/signals.jl index 3de2c2b..c1b6aa9 100644 --- a/src/docgen/signals.jl +++ b/src/docgen/signals.jl @@ -272,10 +272,8 @@ macro signal_table(T, signals...) for signal_id in signals push!(ids, string(signal_id)) - signature = signal_descriptors[signal_id][1] push!(signatures, replace(signature, "T" => string(T))) - push!(descriptions, signal_descriptors[signal_id][2]) end diff --git a/test/example.jl b/test/example.jl index 35453c2..4566518 100644 --- a/test/example.jl +++ b/test/example.jl @@ -1,8 +1,22 @@ # File used for debugging and for dosc examples, why are you snooping through this file? -# File used for debugging and for dosc examples, why are you snooping through this file? using Mousetrap +main() do app::Application + window = Window(app) + aspect_frame = AspectFrame(1.0) + + set_child!(window, aspect_frame) + + connect_signal_motion!(aspect_frame) do _, x, y + println((x,y)) + end + + present!(window) +end + +#== + main() do app::Application window = Window(app) diff --git a/test/runtests.jl b/test/runtests.jl index 4997064..bba8dd0 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -2335,81 +2335,44 @@ end function test_window(::Container) @testset "Window" begin - window = Window(Main.app[]) - other_window = Window(Main.app[]) - - Base.show(devnull, window) - @test Mousetrap.is_native_widget(window) - - close_request_called = Ref{Bool}(false) - connect_signal_close_request!(window, close_request_called) do self::Window, close_request_called - close_request_called[] = true - return WINDOW_CLOSE_REQUEST_RESULT_ALLOW_CLOSE - end - - activate_default_widget_called = Ref{Bool}(false) - connect_signal_activate_default_widget!(window, activate_default_widget_called) do self::Window, activate_default_widget_called - activate_default_widget_called[] = true - return nothing - end - - activate_focused_widget_called = Ref{Bool}(false) - connect_signal_activate_focused_widget!(window, activate_focused_widget_called) do self::Window, activate_focused_widget_called - activate_focused_widget_called[] = true - return nothing - end - - @test get_destroy_with_parent(window) == false - set_destroy_with_parent!(window, true) - @test get_destroy_with_parent(window) == true - @test get_focus_visible(window) == true - set_focus_visible!(window, false) - @test get_focus_visible(window) == false +main() do app::Application + window = Window(app) - @test get_has_close_button(window) == true - set_has_close_button!(window, false) - @test get_has_close_button(window) == false + # create column view with columns + column_view = ColumnView() - @test get_is_decorated(window) == true - set_is_decorated!(window, false) - @test get_is_decorated(window) == false + row_index_column = push_back_column!(column_view, " ") + count_column = push_back_column!(column_view, "#") + name_column = push_back_column!(column_view, "Name") + weight_column = push_back_column!(column_view, "Weight") + unit_column = push_back_column!(column_view, "Units") - @test get_is_modal(window) == false - set_is_modal!(window, true) - @test get_is_modal(window) == true + set_expand!.((row_index, count_column, name_column, weight_column, unit_column), true) - set_title!(window, "test") - @test get_title(window) == "test" + # fill columns with text + for i in 1:100 + row = [ + Label(string(i)), # row index + Label(string(rand(0:99))), # count + Label(rand(["Apple", "Orange", "Banana", "Kumquat", "Durian", "Mangosteen"])), # name + Label(string(rand(0:100))), # weight + Label(string(rand(["mg", "g", "kg", "ton"]))) # unit + ] - button = Entry() - set_child!(window, button) - set_default_widget!(window, button) - activate!(button) - - set_transient_for!(other_window, window) - - #@test activate_default_widget_called[] == true - #@test activate_focused_widget_called[] == true - - @test get_header_bar(window) isa HeaderBar + set_horizontal_alignment!.(row, ALIGNMENT_START) + push_back_row!(column_view, row...) + end - @test get_hide_on_close(window) == false - set_hide_on_close!(window, true) - @test get_hide_on_close(window) == true + # create viewport, this will add a scrollbar + scrolled_viewport = Viewport() + set_propagate_natural_width!(scrolled_viewport, true) # hides horizontal scrollbar + set_child!(scrolled_viewport, column_view) - @test get_is_closed(window) == true - present!(window) - @test get_is_closed(window) == false - set_minimized!(window, true) - set_maximized!(window, true) + # show both in window + set_child!(window, scrolled_viewport) + present!(window) - close!(other_window) - close!(window) - @test get_is_closed(window) == true - destroy!(window) - destroy!(other_window) - end end ### WIDGET @@ -2803,7 +2766,6 @@ end main(Main.app_id) do app::Application - #= Main.app[] = app Main.window[] = Window(app) set_is_decorated!(window, false) # prevent user from closing the window during tests @@ -2889,78 +2851,7 @@ main(Main.app_id) do app::Application present!(window) close!(window) #quit!(app) - =# - - window = Window(Main.app[]) - Base.show(devnull, window) - @test Mousetrap.is_native_widget(window) - - close_request_called = Ref{Bool}(false) - connect_signal_close_request!(window, close_request_called) do self::Window, close_request_called - close_request_called[] = true - return WINDOW_CLOSE_REQUEST_RESULT_ALLOW_CLOSE - end - - activate_default_widget_called = Ref{Bool}(false) - connect_signal_activate_default_widget!(window, activate_default_widget_called) do self::Window, activate_default_widget_called - activate_default_widget_called[] = true - return nothing - end - - activate_focused_widget_called = Ref{Bool}(false) - connect_signal_activate_focused_widget!(window, activate_focused_widget_called) do self::Window, activate_focused_widget_called - activate_focused_widget_called[] = true - return nothing - end - - @test get_destroy_with_parent(window) == false - set_destroy_with_parent!(window, true) - @test get_destroy_with_parent(window) == true - - @test get_focus_visible(window) == true - set_focus_visible!(window, false) - @test get_focus_visible(window) == false - - @test get_has_close_button(window) == true - set_has_close_button!(window, false) - @test get_has_close_button(window) == false - - @test get_is_decorated(window) == true - set_is_decorated!(window, false) - @test get_is_decorated(window) == false - - @test get_is_modal(window) == false - set_is_modal!(window, true) - @test get_is_modal(window) == true - - set_title!(window, "test") - @test get_title(window) == "test" - - button = Entry() - set_child!(window, button) - set_default_widget!(window, button) - activate!(button) - - #@test activate_default_widget_called[] == true - #@test activate_focused_widget_called[] == true - - @test get_header_bar(window) isa HeaderBar - - @test get_hide_on_close(window) == false - set_hide_on_close!(window, true) - @test get_hide_on_close(window) == true - - other_window = Window(app) - set_transient_for!(other_window, window) - @test get_is_closed(window) == true - present!(window) - @test get_is_closed(window) == false - set_minimized!(window, true) - set_maximized!(window, true) - close!(window) - @test get_is_closed(window) == true - destroy!(window) println("TODO: done.") end From 4f16e9fbf35728a536fabe323f8fed8cf1bce79f Mon Sep 17 00:00:00 2001 From: Clem Cords Date: Thu, 8 Feb 2024 18:18:59 +0100 Subject: [PATCH 07/31] improved error message when a compound widgets toplevel does not have a signal with given name --- src/Mousetrap.jl | 37 +++++++++++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/src/Mousetrap.jl b/src/Mousetrap.jl index e2708b1..fc3bfa0 100644 --- a/src/Mousetrap.jl +++ b/src/Mousetrap.jl @@ -5543,13 +5543,21 @@ end # else MOUSETRAP_ENABLE_OPENGL_COMPONENT connect_signal_name = :connect_signal_ * signal * :! push!(out.args, esc(:( function Mousetrap.$connect_signal_name(f, x::Widget) - $connect_signal_name(f, Mousetrap.get_top_level_widget(x)) + if Mousetrap.is_native_widget(x) + log_critical(Mousetrap.MOUSETRAP_DOMAIN, "In " * $(string(connect_signal_name)) * ": object of type `$(typeof(x))` has no signal with ID `" * $(string(signal)) * "`") + else + $connect_signal_name(f, Mousetrap.get_top_level_widget(x)) + end end ))) push!(out.args, esc(:( function Mousetrap.$connect_signal_name(f, x::Widget, data::Data_t) where Data_t - $connect_signal_name(f, Mousetrap.get_top_level_widget(x), data) + if Mousetrap.is_native_widget(x) + log_critical(Mousetrap.MOUSETRAP_DOMAIN, "In " * $(string(connect_signal_name)) * ": object of type `$(typeof(x))` has no signal with ID `" * $(string(signal)) * "`") + else + $connect_signal_name(f, Mousetrap.get_top_level_widget(x), data) + end end ))) @@ -5557,7 +5565,11 @@ end # else MOUSETRAP_ENABLE_OPENGL_COMPONENT push!(out.args, esc(:( function $emit_signal_name(x::Widget, args...) - $emit_signal_name(Mousetrap.get_top_level_widget(x), args) + if Mousetrap.is_native_widget(x) + log_critical(Mousetrap.MOUSETRAP_DOMAIN, "In " * $(string(emit_signal_name)) * ": object of type `$(typeof(x))` has no signal with ID `" * $(string(signal)) * "`") + else + $emit_signal_name(Mousetrap.get_top_level_widget(x), args) + end end ))) @@ -5565,7 +5577,11 @@ end # else MOUSETRAP_ENABLE_OPENGL_COMPONENT push!(out.args, esc(:( function $disconnect_signal_name(x::Widget) - $disconnect_signal_name(Mousetrap.get_top_level_widget(x)) + if Mousetrap.is_native_widget(x) + log_critical(Mousetrap.MOUSETRAP_DOMAIN, "In " * $(string(disconnect_signal_name)) * ": object of type `$(typeof(x))` has no signal with ID `" * $(string(signal)) * "`") + else + $disconnect_signal_name(Mousetrap.get_top_level_widget(x)) + end end ))) @@ -5573,7 +5589,11 @@ end # else MOUSETRAP_ENABLE_OPENGL_COMPONENT push!(out.args, esc(:( function $set_signal_blocked_name(x::Widget, b) - $set_signal_blocked_name(Mousetrap.get_top_level_widget(x), b) + if Mousetrap.is_native_widget(x) + log_critical(Mousetrap.MOUSETRAP_DOMAIN, "In " * $(string(set_signal_blocked_name)) * ": object of type `$(typeof(x))` has no signal with ID `" * $(string(signal)) * "`") + else + $set_signal_blocked_name(Mousetrap.get_top_level_widget(x), b) + end end ))) @@ -5581,7 +5601,12 @@ end # else MOUSETRAP_ENABLE_OPENGL_COMPONENT push!(out.args, esc(:( function $get_signal_blocked_name(x::Widget) - return $get_signal_blocked_name(Mousetrap.get_top_level_widget(x)) + if Mousetrap.is_native_widget(x) + log_critical(Mousetrap.MOUSETRAP_DOMAIN, "In " * $(string(get_signal_blocked_name)) * ": object of type `$(typeof(x))` has no signal with ID `" * $(string(signal)) * "`") + return false + else + return $get_signal_blocked_name(Mousetrap.get_top_level_widget(x)) + end end ))) end From 2eca1b2eb9bed4fc8090df28402fde1b1dcd7351 Mon Sep 17 00:00:00 2001 From: Clem Cords Date: Thu, 8 Feb 2024 18:30:41 +0100 Subject: [PATCH 08/31] added scaling default value, #70 --- src/Mousetrap.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Mousetrap.jl b/src/Mousetrap.jl index fc3bfa0..3837be1 100644 --- a/src/Mousetrap.jl +++ b/src/Mousetrap.jl @@ -1897,7 +1897,7 @@ module Mousetrap @export_function Image get_n_pixels Int64 @export_function Image get_size Vector2i - function as_scaled(image::Image, size_x::Integer, size_y::Integer, interpolation::InterpolationType) + function as_scaled(image::Image, size_x::Integer, size_y::Integer, interpolation::InterpolationType = INTERPOLATION_TYPE_TILES) return Image(detail.as_scaled(image._internal, UInt64(size_x), UInt64(size_y), interpolation)) end export as_scaled From 77d4a24fd77ee365ee976d4dd72b532e74695c61 Mon Sep 17 00:00:00 2001 From: Clem Cords Date: Thu, 8 Feb 2024 18:40:36 +0100 Subject: [PATCH 09/31] Added section on infinitely long animations #63 --- docs/src/01_manual/10_theme_customization.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/src/01_manual/10_theme_customization.md b/docs/src/01_manual/10_theme_customization.md index 6c22519..59cb5d7 100644 --- a/docs/src/01_manual/10_theme_customization.md +++ b/docs/src/01_manual/10_theme_customization.md @@ -140,6 +140,11 @@ We can then start the animation using `play!`, for example, by clicking the butt For cyclical animations, we can use [`set_repeat_count!`](@ref) to specify the number of times the animation should loop, or `0` to loop infinitely. We can easily reverse an animation by setting [`set_is_reversed!`](@ref) to `true`. + +If we want an animation to run indefinitely, but *not* repeat, for example to animate an object continuously bouncing, setting the animation duration to a very large value is not right way to achieve this, as it [may cause side-effects](https://github.com/Clemapfel/Mousetrap.jl/issues/63). Instead, use [`set_tick_callback!`](@ref). This function registers a callback to be invoked once per frame in synch with the corresponding widgets render cycle. By keeping track of the elapsed time, we can time animations this way without using `Animation`. The tick callback will continue until a value other than `TICK_CALLBACK_RESULT_CONTINUE` is returned. For an example of converting a regular `Animation`-based animation to tick callback, see [here](https://github.com/Clemapfel/Mousetrap.jl/issues/63). + +`Animation` should be reserved to short "one-shot" animations such as an object sliding or spinning, or to cyclical animations, which are just those short animations repeating. + Attentive readers may remember that pre-made animations for `Stack` and `Revealer` also include widgets spinning or moving around the screen. So far, we have no good way of implementing motion like this. This is about to change. ## TransformBin From b7d7ed81bc4fad537e6a8ea06e1abbb56fe2373d Mon Sep 17 00:00:00 2001 From: Clem Cords Date: Thu, 8 Feb 2024 18:51:26 +0100 Subject: [PATCH 10/31] typo #61 --- docs/src/01_manual/04_widgets.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/01_manual/04_widgets.md b/docs/src/01_manual/04_widgets.md index 199e936..f1513ff 100644 --- a/docs/src/01_manual/04_widgets.md +++ b/docs/src/01_manual/04_widgets.md @@ -1479,7 +1479,7 @@ Apart from the speed, we also have a choice of animation **type**, represented b # create a button that, when clicked, triggers the revealer animation button = Button() connect_signal_clicked!(button, revealer) do self::Button, revealer::Revealer - set_is_revealed!(revealer, !get_revealed(revealer)) + set_is_revealed!(revealer, !get_is_revealed(revealer)) end set_child!(window, vbox(button, revealer)) From 62506e31a81b8ecad4f0dbe915604b0344a80a0a Mon Sep 17 00:00:00 2001 From: Clem Cords Date: Thu, 8 Feb 2024 19:20:53 +0100 Subject: [PATCH 11/31] typo --- src/Mousetrap.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Mousetrap.jl b/src/Mousetrap.jl index 3837be1..a642bb2 100644 --- a/src/Mousetrap.jl +++ b/src/Mousetrap.jl @@ -361,7 +361,7 @@ module Mousetrap @generated function get_top_level_widget(x) ::Widget return :( - throw(AssertionError("Object of type $(typeof(x)) does not fulfill the widget interface. In order for it to be able to be treated as a widget, you need to subtype `Mousetrap.Widget` **and** add a method with signature `(::$(typeof(x))) -> Widget` to `Mousetrap.get_top_level_widget`, which should map an instance of $(typeof(x)) to its top-level widget component.")) + throw(AssertionError("Object of type $(typeof(x)) does not fullfill the widget interface. In order for it to be able to be treated as a widget, you need to subtype `Mousetrap.Widget` **and** add a method with signature `(::$(typeof(x))) -> Widget` to `Mousetrap.get_top_level_widget`, which should map an instance of $(typeof(x)) to its top-level widget component.")) ) end export get_top_level_widget From 002d7303d3532d0784f6effe5c2ffc433b133dfa Mon Sep 17 00:00:00 2001 From: Clem Cords Date: Thu, 8 Feb 2024 19:26:33 +0100 Subject: [PATCH 12/31] fixed n digits conversion for enums #69 --- src/Mousetrap.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Mousetrap.jl b/src/Mousetrap.jl index a642bb2..4d4e51b 100644 --- a/src/Mousetrap.jl +++ b/src/Mousetrap.jl @@ -444,8 +444,9 @@ module Mousetrap enum_sym = QuoteNode(enum) to_int_name = Symbol(enum) * :_to_int + push!(out.args, :(Base.ndigits(x::$enum) = ndigits(Mousetrap.detail.$to_int_name(x)))) push!(out.args, :(Base.string(x::$enum) = string(Mousetrap.detail.$to_int_name(x)))) - push!(out.args, :(Base.convert(::Type{Integer}, x::$enum) = Integer(Mousetrap.detail.to_int_name(x)))) + push!(out.args, :(Base.convert(::Type{Integer}, x::$enum) = Integer(Mousetrap.detail.$to_int_name(x)))) push!(out.args, :(Base.instances(x::Type{$enum}) = [$(names...)])) push!(out.args, :(Base.show(io::IO, x::Type{$enum}) = print(io, (isdefined(Main, $enum_sym) ? "" : "Mousetrap.") * $enum_str))) push!(out.args, :(Base.show(io::IO, x::$enum) = print(io, string($enum) * "(" * string(convert(Int64, x)) * ")"))) From a4c59a4376bc0d1a9c6c8d1fe2db44d73cba8307 Mon Sep 17 00:00:00 2001 From: Clem Cords Date: Sun, 11 Feb 2024 17:55:07 +0100 Subject: [PATCH 13/31] added separator opacity argument --- src/Mousetrap.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Mousetrap.jl b/src/Mousetrap.jl index 4d4e51b..011fe56 100644 --- a/src/Mousetrap.jl +++ b/src/Mousetrap.jl @@ -444,7 +444,7 @@ module Mousetrap enum_sym = QuoteNode(enum) to_int_name = Symbol(enum) * :_to_int - push!(out.args, :(Base.ndigits(x::$enum) = ndigits(Mousetrap.detail.$to_int_name(x)))) + #push!(out.args, :(Base.ndigits(x::$enum) = ndigits(Mousetrap.detail.$to_int_name(x)))) push!(out.args, :(Base.string(x::$enum) = string(Mousetrap.detail.$to_int_name(x)))) push!(out.args, :(Base.convert(::Type{Integer}, x::$enum) = Integer(Mousetrap.detail.$to_int_name(x)))) push!(out.args, :(Base.instances(x::Type{$enum}) = [$(names...)])) @@ -4534,7 +4534,7 @@ module Mousetrap @export_type Separator Widget @declare_native_widget Separator - Separator(orientation::Orientation = ORIENTATION_HORIZONTAL) = Separator(detail._Separator(orientation)) + Separator(orientation::Orientation = ORIENTATION_HORIZONTAL; opacity = 1.0) = Separator(detail._Separator(orientation)) @export_function Separator set_orientation! Cvoid Orientation orientation @export_function Separator get_orientation Orientation From b72910a73078873423d68faeea98ac52041a7aa4 Mon Sep 17 00:00:00 2001 From: Clem Cords Date: Sun, 11 Feb 2024 18:07:21 +0100 Subject: [PATCH 14/31] doctests working --- docs/src/01_manual/01_installation.md | 21 ++- docs/src/01_manual/02_signals.md | 22 +++- docs/src/01_manual/03_actions.md | 19 ++- docs/src/01_manual/04_widgets.md | 130 ++++++++++++++----- docs/src/01_manual/05_event_handling.md | 14 ++ docs/src/01_manual/06_image.md | 14 ++ docs/src/01_manual/07_os_interface.md | 14 ++ docs/src/01_manual/08_menus.md | 14 ++ docs/src/01_manual/09_native_rendering.md | 14 ++ docs/src/01_manual/10_theme_customization.md | 14 ++ docs/src/01_manual/11_app_distribution.md | 14 ++ docs/src/01_manual/12_opengl_integration.md | 14 ++ docs/src/02_library/classes.md | 2 + docs/src/02_library/functions.md | 4 + 14 files changed, 269 insertions(+), 41 deletions(-) diff --git a/docs/src/01_manual/01_installation.md b/docs/src/01_manual/01_installation.md index 325e14c..400222e 100644 --- a/docs/src/01_manual/01_installation.md +++ b/docs/src/01_manual/01_installation.md @@ -1,3 +1,17 @@ +```@meta +DocTestSetup = quote + using Mousetrap + function Window(app::Application) + out = Mousetrap.Window(app) + set_tick_callback!(out, out) do clock, self + close!(self) + return TICK_CALLBACK_RESULT_DISCONTINUE + end + return out + end +end +``` + # Chapter 1: Installation & Workflow In this chapter, we will learn: @@ -9,10 +23,8 @@ In this chapter, we will learn: ## Installation - To install Mousetrap, in the REPL, press the `]` key, then - ```julia import Pkg; begin @@ -41,13 +53,16 @@ Installation may take a long time. Once installation is succesfull, `Mousetrap t To create our first Mousetrap app, we create a Julia file `main.jl`, with the following contents: -```julia +```jldoctest; output = false using Mousetrap main() do app::Application window = Window(app) set_child!(window, Label("Hello World!")) present!(window) end + +# output +0 ``` To start our app, we navigate to the location of `main.jl` in our console, then execute: diff --git a/docs/src/01_manual/02_signals.md b/docs/src/01_manual/02_signals.md index dd9aa8e..1ba975c 100644 --- a/docs/src/01_manual/02_signals.md +++ b/docs/src/01_manual/02_signals.md @@ -1,3 +1,17 @@ +```@meta +DocTestSetup = quote + using Mousetrap + function Window(app::Application) + out = Mousetrap.Window(app) + set_tick_callback!(out, out) do clock, self + close!(self) + return TICK_CALLBACK_RESULT_DISCONTINUE + end + return out + end +end +``` + # Chapter 2: Signals In this chapter, we will learn: @@ -63,7 +77,7 @@ end For example, to execute the example snippet above, we would create the following `main.jl` file: - ```julia + ```jldoctest; output = false using Mousetrap main() do app::Application window = Window(app) @@ -78,6 +92,8 @@ end set_child!(window, button) # add the button to the window present!(window) end + # output + 0 ``` Then execute it from the console by calling `julia main.jl` @@ -494,7 +510,7 @@ By blocking signals, we get the correct behavior of both buttons being triggered To verify this is indeed the resulting behavior, we can use the following `main.jl`: -```julia +```jldoctest; output = false using Mousetrap main() do app::Application window = Window(app) @@ -521,6 +537,8 @@ main() do app::Application set_child!(window, hbox(button_01, button_02)) present!(window) end +# output +0 ``` ![](../assets/double_button_signal_blocking.png) diff --git a/docs/src/01_manual/03_actions.md b/docs/src/01_manual/03_actions.md index a4e8561..0a9c90a 100644 --- a/docs/src/01_manual/03_actions.md +++ b/docs/src/01_manual/03_actions.md @@ -1,3 +1,17 @@ +```@meta +DocTestSetup = quote + using Mousetrap + function Window(app::Application) + out = Mousetrap.Window(app) + set_tick_callback!(out, out) do clock, self + close!(self) + return TICK_CALLBACK_RESULT_DISCONTINUE + end + return out + end +end +``` + # Chapter 3: Actions In this chapter, we will learn: @@ -222,10 +236,9 @@ We need one more thing before we can trigger our action: an object that can rece A complete `main.jl` file showing how to trigger an action using a shortcut is given here: -```julia +```jldoctest; output = false using Mousetrap main() do app::Application - # create a window window = Window(app) @@ -243,6 +256,8 @@ main() do app::Application # show the window to the user present!(window) end +# output +0 ``` Pressing "Control + M", we get: diff --git a/docs/src/01_manual/04_widgets.md b/docs/src/01_manual/04_widgets.md index f1513ff..44d914a 100644 --- a/docs/src/01_manual/04_widgets.md +++ b/docs/src/01_manual/04_widgets.md @@ -1,3 +1,17 @@ +```@meta +DocTestSetup = quote + using Mousetrap + function Window(app::Application) + out = Mousetrap.Window(app) + set_tick_callback!(out, out) do clock, self + close!(self) + return TICK_CALLBACK_RESULT_DISCONTINUE + end + return out + end +end +``` + # Chapter 4: Widgets In this chapter, we will learn: @@ -325,7 +339,7 @@ While windows are widgets, they occupy a somewhat of a special place. `Window` i When we create a `Window` instance, it will be initially hidden. None of its children will be realized or shown, and the user has no way to know that the window exists. A `Window`s lifetime only begins once we call [`present!`](@ref). This opens the window and shows it to the user, realizing all its children. We've seen this in our `main` functions before: -```julia +```jldoctest; output = false main() do app::Application # create the window @@ -334,6 +348,8 @@ main() do app::Application # show the window, this realizes all widgets inside present!(window) end +# output +0 ``` At any point, we can call `close!`, which hides the window. This does not destroy the window permanently unless [`set_hide_on_close!`](@ref) was set to `false` previously, we can `present!` to show the window again. For an application to exit, all its windows only need to be hidden, not permanently destroyed. Therefore, calling `close!` on all windows may cause the application to attempt to exit. @@ -700,7 +716,7 @@ We can manually emit this signal using `emit_signal_clicked`, or by calling `act ![](../assets/button_types.png) !!! details "How to generate this image" - ```julia + ```jldoctest; output = false using Mousetrap main() do app::Application window = Window(app) @@ -720,8 +736,10 @@ We can manually emit this signal using `emit_signal_clicked`, or by calling `act set_margin!(box, 75) set_child!(window, box) - pesent!(window) + present!(window) end + # output + 0 ``` Where the above-shown buttons have the following properties: @@ -776,7 +794,7 @@ Mousetrap.@signal_table(ToggleButton, ![](../assets/check_button_states.png) !!! details "How to generate this image" - ```julia + ```jldoctest; output = false using Mousetrap main() do app::Application @@ -804,6 +822,8 @@ Mousetrap.@signal_table(ToggleButton, set_child!(window, box) present!(window) end + # output + 0 ``` Note that `get_is_active` will only return `true` if the current state is specifically `CHECK_BUTTON_STATE_ACTIVE`. `toggled` is emitted whenever the state changes, regardless of which state the `CheckButton` was in. @@ -824,7 +844,7 @@ Mousetrap.@signal_table(Switch, ![](../assets/switch_states.png) !!! details "How to generate this image" - ```julia + ```jldoctest; output = false main() do app::Application window = Window(app) @@ -850,6 +870,8 @@ Mousetrap.@signal_table(Switch, set_child!(window, box) present!(window) end + # output + 0 ``` --- @@ -908,7 +930,7 @@ spin_button = SpinButton(0, 2, 0.5) ![](../assets/spin_button.png) !!! details "How to generate this image" - ```julia + ```jldoctest; output = false main() do app::Application window = Window(app) @@ -933,7 +955,9 @@ spin_button = SpinButton(0, 2, 0.5) set_child!(window, box) present!(window) - end + end + # output + 0 ``` We set and access any property of spin button using `get_value`, `set_value!`, `get_lower`, `set_lower!`, etc. These work exactly as if we were modifying the underlying `Adjustment`, which we can also obtain using `get_adjustment`. @@ -956,7 +980,7 @@ The other signal is `wrapped`, which is emitted when [`set_should_wrap!`](@ref) ![](../assets/scale_no_value.png) !!! details "How to generate this image" - ```julia + ```jldoctest; output = false main() do app::Application window = Window(app) @@ -980,6 +1004,8 @@ The other signal is `wrapped`, which is emitted when [`set_should_wrap!`](@ref) set_child!(window, box) present!(window) end + # output + 0 ``` [`Scale`](@ref), just like `SpinButton`, is a widget that allows a user to choose a value from the underlying `Adjustment`. This is done by click-dragging the knob of the scale or clicking anywhere on its rail. In this way, it is usually harder to pick an exact decimal value on a `Scale` as opposed to a `SpinButton`. We can aid in this task by displaying the exact value next to the scale, which is enabled with [`set_should_draw_value!`](@ref): @@ -987,12 +1013,10 @@ The other signal is `wrapped`, which is emitted when [`set_should_wrap!`](@ref) ![](../assets/scale_with_value.png) !!! details "How to generate this image" - ```julia + ```jldoctest; output = false main() do app::Application window = Window(app) - window = Window(app) - horizontal = Scale(0, 2, 0.5) set_orientation!(horizontal, ORIENTATION_HORIZONTAL) set_value!(horizontal, 1) @@ -1015,6 +1039,8 @@ The other signal is `wrapped`, which is emitted when [`set_should_wrap!`](@ref) set_child!(window, box) present!(window) end + # output + 0 ``` `Scale` supports most of `SpinButton`'s functions, including querying information about its underlying range, setting the orientation, and signal `value_changed`: @@ -1046,7 +1072,7 @@ Once the bar reaches 75%, it changes color: ![](../assets/level_bar.png) !!! details "How to generate this image" - ```julia + ```jldoctest; output = false main() do app::Application window = Window(app) @@ -1075,6 +1101,8 @@ Once the bar reaches 75%, it changes color: set_child!(window, box) present!(window) end + # output + 0 ``` `LevelBar` also supports displaying a discrete value, in which case it will be drawn segmented. To enable this, we set [`set_mode!`](@ref) to `LEVEL_BAR_DISPLAY_MODE_DISCRETE`, as opposed to `LEVEL_BAR_MODE_CONTINUOUS`, which is the default. @@ -1092,7 +1120,7 @@ Using `set_show_text!`, we can make it so the current percentage is drawn along ![](../assets/progress_bar.png) !!! details "How to generate this image" - ```julia + ```jldoctest; output = false main() do app::Application window = Window(app) @@ -1108,6 +1136,8 @@ Using `set_show_text!`, we can make it so the current percentage is drawn along set_child!(window, progress_bar) present!(window) end + # output + 0 ``` --- @@ -1118,7 +1148,7 @@ To signal progress when we do not have an exact fraction, we use [`Spinner`](@re ![](../assets/spinner.png) !!! details "How to generate this image" - ```julia + ```jldoctest; output = false main() do app::Application window = Window(app) @@ -1129,7 +1159,9 @@ To signal progress when we do not have an exact fraction, we use [`Spinner`](@re set_child!(window, spinner) present!(window) end - ``` + # output + 0 + ``` --- @@ -1148,7 +1180,7 @@ To enter password mode, we set [`set_text_visible!`](@ref) to `false`. Note that ![](../assets/entry.png) !!! details "How to generate this image" - ```julia + ```jldoctest; output = false main() do app::Application window = Window(app) @@ -1168,6 +1200,8 @@ To enter password mode, we set [`set_text_visible!`](@ref) to `false`. Note that set_child!(window, box) present!(window) end + # output + 0 ``` Lastly, `Entry` is **activatable**, when the user presses the enter key while the cursor is inside the entries text area, it will emit signal `activate`. Its other signal, `text_changed`, is emitted whenever the internal text buffer changes: @@ -1283,7 +1317,7 @@ Where we had to first create a `Label` instance, then use it as the label widget ![](../assets/frame.png) !!! details "How to generate this image" - ```julia + ```jldoctest; output = false main() do app::Application window = Window(app) @@ -1306,6 +1340,8 @@ Where we had to first create a `Label` instance, then use it as the label widget set_child!(window, box) present!(window) end + # output + 0 ``` Using [`set_label_widget!`](@ref), we can furthermore choose a widget to be displayed above the child widget of the frame. This will usually be a `Label`, though `set_label_widget!` accepts any kind of widget. @@ -1347,7 +1383,6 @@ In which we use two nested `ClampFrame`s, such that `child_widget` can never exc --- - ## Overlay So far, all widget containers have aligned their children such that they do not overlap. In cases where we do want this to happen, for example, if we want to render one widget in front of another, we have to use [`Overlay`](@ref). @@ -1357,7 +1392,7 @@ So far, all widget containers have aligned their children such that they do not ![](../assets/overlay_buttons.png) !!! details "How to generate this image" - ```julia + ```jldoctest; output = false main() do app::Application window = Window(app) @@ -1377,6 +1412,8 @@ So far, all widget containers have aligned their children such that they do not set_child!(window, AspectFrame(1, overlay)) present!(window) end + # output + 0 ``` Where the position and size of overlayed widgets depend on their expansion and alignment properties. @@ -1398,7 +1435,7 @@ add_overlay!(overlay, overlaid_widge; include_in_measurement = true) ![](../assets/paned.png) !!! details "How to generate this image" - ```julia + ```jldoctest; output = false function generate_child(label::String) out = Frame(Overlay(Separator(), Label(label))) set_margin!(out, 10) @@ -1419,6 +1456,8 @@ add_overlay!(overlay, overlaid_widge; include_in_measurement = true) set_child!(window, paned) present!(window) end + # output + 0 ``` `Paned` has two per-child properties: whether a child is **resizable** and whether it is **shrinkable**. @@ -1461,7 +1500,7 @@ Apart from the speed, we also have a choice of animation **type**, represented b ![](../assets/revealer.png) !!! details "How to generate this image" - ```julia + ```jldoctest; output = false main() do app::Application window = Window(app) @@ -1485,6 +1524,8 @@ Apart from the speed, we also have a choice of animation **type**, represented b set_child!(window, vbox(button, revealer)) present!(window) end + # output + 0 ``` --- @@ -1505,7 +1546,7 @@ Expander has two children, the label, set with `set_label_widget!`, and its chil ![](../assets/expander.png) !!! details "How to generate this image" - ```julia + ```jldoctest; output = false main() do app::Application window = Window(app) @@ -1522,6 +1563,8 @@ Expander has two children, the label, set with `set_label_widget!`, and its chil set_child!(window, expander_and_frame) present!(window) end + # output + 0 ``` Note that `Expander` should not be used to create nested lists, as `ListView`, a widget we will learn about later in this chapter, is better suited for this purpose. @@ -1539,7 +1582,7 @@ We set the viewport's singular child using `set_child!`, after which the user ca ![](../assets/viewport.png) !!! details "How to generate this image" - ```julia + ```jldoctest; output = false main() do app::Application window = Window(app) @@ -1553,6 +1596,8 @@ We set the viewport's singular child using `set_child!`, after which the user ca set_child!(window, viewport) present!(window) end + # output + 0 ``` ### Size Propagation @@ -1635,7 +1680,7 @@ Showing the popover is called **popup**, closing the popover is called **popdown ![](../assets/popover.png) !!! details "How to generate this image" - ```julia + ```jldoctest; output = false using Mousetrap main() do app::Application @@ -1658,6 +1703,8 @@ Showing the popover is called **popup**, closing the popover is called **popdown set_child!(window, vbox(popover, button)) present!(window) end + # output + 0 ``` Manually calling `popup!` or `popdown!` to show or hide the `Popover` can be bothersome. To address this, Mousetrap offers a widget that automatically manages the popover for us: [`PopoverButton`](@ref) @@ -1737,7 +1784,7 @@ By default, `ListView` displays its children in a linear list, either horizontal ![](../assets/list_view_nested.png) !!! details "How to generate this image" - ```julia + ```jldoctest; output = false main() do app::Application window = Window(app) @@ -1757,6 +1804,8 @@ By default, `ListView` displays its children in a linear list, either horizontal set_child!(window, frame) present!(window) end + # output + 0 ``` The user can click any item to hide or show its children. Items that do not have any children will appear just as with a non-nested `ListView`. @@ -1799,7 +1848,7 @@ If we do not want a nested list, we can instead completely ignore the iterator. *A vertical `GridView`* !!! details "How to generate this image" - ```julia + ```jldoctest; output = false function generate_child(label::String) ::Widget child = Frame(Overlay(Separator(), Label(label))) @@ -1825,6 +1874,8 @@ If we do not want a nested list, we can instead completely ignore the iterator. set_child!(window, vbox(separator, grid_view)) present!(window) end + # output + 0 ``` We can control the exact distribution of widgets more closely by using [`set_max_n_columns!`](@ref) and [`set_min_n_columns!`](@ref), which make it so the grid view will always have the given number of columns (or rows, for a horizontal `GridView`). @@ -1861,11 +1912,9 @@ set_widget_at!(column_view, column_01, Label("03")) ![](../assets/column_view.png) !!! details "How to generate this image" - ```julia + ```jldoctest; output = false main() do app::Application - println("called") - window = Window(app) set_title!(window, "Mousetrap.jl") @@ -1887,6 +1936,8 @@ set_widget_at!(column_view, column_01, Label("03")) set_child!(window, column_view) present!(window) end + # output + 0 ``` Any rows that do not yet have widgets will be backfilled and appear empty. @@ -1946,7 +1997,7 @@ to provide another widget that modifies the stack, or we can use one of two pre- ![](../assets/stack_switcher.png) !!! details "How to generate this image" - ```julia + ```jldoctest; output = false function generate_child(label::String) ::Widget child = Frame(Overlay(Separator(), Label(label))) set_size_request!(child, Vector2f(150, 150)) @@ -1971,6 +2022,8 @@ to provide another widget that modifies the stack, or we can use one of two pre- set_child!(window, vbox(stack, StackSwitcher(stack))) # create StackSwitcher from stack present!(window) end + # output + 0 ``` `StackSwitcher` has no other methods or properties, though it provides the signals that all widgets share. @@ -1980,7 +2033,7 @@ to provide another widget that modifies the stack, or we can use one of two pre- [`StackSidebar`](@ref) has the same purpose as `StackSwitcher`, except it displays the list of stack pages as a vertical list: !!! details "How to generate this image" - ```julia + ```jldoctest; output = false function generate_child(label::String) ::Widget child = Frame(Overlay(Separator(), Label(label))) set_size_request!(child, Vector2f(150, 150)) @@ -2005,6 +2058,8 @@ to provide another widget that modifies the stack, or we can use one of two pre- set_child!(window, vbox(stack, StackSwitcher(stack))) # Create StackSidebar from stack present!(window) end + # output + 0 ``` Other than this visual component, its purpose is identical to that of `StackSwitcher`. @@ -2030,7 +2085,7 @@ Not to be confused with `GridView`, [`Grid`](@ref) arranges its children in a ** ![](../assets/grid.png) !!! details "How to generate this image" - ```julia + ```jldoctest; output = false function generate_child(label::String) ::Widget child = Frame(Overlay(Separator(), Label(label))) set_size_request!(child, Vector2f(50, 50)) @@ -2055,6 +2110,9 @@ Not to be confused with `GridView`, [`Grid`](@ref) arranges its children in a ** set_child!(window, grid) present!(window) end + + # output + 0 ``` Each widget in the grid has an x- and y-position, along with a widget and height, both measured in **number of cells** . @@ -2128,7 +2186,7 @@ push_back!(notebook, child_03, Label("Label #03")) ![](../assets/notebook.png) !!! details "How to generate this image" - ```julia + ```jldoctest; output = false function generate_child(label::String) ::Widget child = Frame(Overlay(Separator(), Label(label))) set_size_request!(child, Vector2f(150, 150)) @@ -2148,6 +2206,8 @@ push_back!(notebook, child_03, Label("Label #03")) set_child!(window, notebook) present!(window) end + # output + 0 ``` We can allow the user to reorder the pages by setting [`set_tabs_reorderable!`](@ref) to `true`. This will make it so they can click drag-and-drop the label widget, moving the entire notebook page to that position. @@ -2302,7 +2362,7 @@ Where the `Mousetrap.` prefix is necessary to tell the compiler that we are over With `Widget` subtyped and `get_top_level_widget` implemented, our `main.jl` now looks like this: -```julia +```jldoctest; output = false struct Placeholder <: Widget label::Label separator::Separator @@ -2334,6 +2394,8 @@ main() do app::Application set_child!(window, Placeholder("TEST")) present!(window) end +# output +0 ``` ![](../assets/compound_widget_complete.png) diff --git a/docs/src/01_manual/05_event_handling.md b/docs/src/01_manual/05_event_handling.md index eaaa62c..187e5bd 100644 --- a/docs/src/01_manual/05_event_handling.md +++ b/docs/src/01_manual/05_event_handling.md @@ -1,3 +1,17 @@ +```@meta +DocTestSetup = quote + using Mousetrap + function Window(app::Application) + out = Mousetrap.Window(app) + set_tick_callback!(out, out) do clock, self + close!(self) + return TICK_CALLBACK_RESULT_DISCONTINUE + end + return out + end +end +``` + # Chapter 5: Event Handling In this chapter, we will learn: diff --git a/docs/src/01_manual/06_image.md b/docs/src/01_manual/06_image.md index 29706ab..1c87c5d 100644 --- a/docs/src/01_manual/06_image.md +++ b/docs/src/01_manual/06_image.md @@ -1,3 +1,17 @@ +```@meta +DocTestSetup = quote + using Mousetrap + function Window(app::Application) + out = Mousetrap.Window(app) + set_tick_callback!(out, out) do clock, self + close!(self) + return TICK_CALLBACK_RESULT_DISCONTINUE + end + return out + end +end +``` + # Chapter 6: Images In this chapter, we will learn: diff --git a/docs/src/01_manual/07_os_interface.md b/docs/src/01_manual/07_os_interface.md index 1378588..1b90100 100644 --- a/docs/src/01_manual/07_os_interface.md +++ b/docs/src/01_manual/07_os_interface.md @@ -1,3 +1,17 @@ +```@meta +DocTestSetup = quote + using Mousetrap + function Window(app::Application) + out = Mousetrap.Window(app) + set_tick_callback!(out, out) do clock, self + close!(self) + return TICK_CALLBACK_RESULT_DISCONTINUE + end + return out + end +end +``` + # Chapter 7: Operating System Interface In this chapter, we will learn: diff --git a/docs/src/01_manual/08_menus.md b/docs/src/01_manual/08_menus.md index c246a4e..67b54ba 100644 --- a/docs/src/01_manual/08_menus.md +++ b/docs/src/01_manual/08_menus.md @@ -1,3 +1,17 @@ +```@meta +DocTestSetup = quote + using Mousetrap + function Window(app::Application) + out = Mousetrap.Window(app) + set_tick_callback!(out, out) do clock, self + close!(self) + return TICK_CALLBACK_RESULT_DISCONTINUE + end + return out + end +end +``` + # Chapter 8: Menus In this chapter, we will learn: diff --git a/docs/src/01_manual/09_native_rendering.md b/docs/src/01_manual/09_native_rendering.md index 08c8d9c..84335d6 100644 --- a/docs/src/01_manual/09_native_rendering.md +++ b/docs/src/01_manual/09_native_rendering.md @@ -1,3 +1,17 @@ +```@meta +DocTestSetup = quote + using Mousetrap + function Window(app::Application) + out = Mousetrap.Window(app) + set_tick_callback!(out, out) do clock, self + close!(self) + return TICK_CALLBACK_RESULT_DISCONTINUE + end + return out + end +end +``` + # Chapter 9: Native Rendering In this chapter we will learn: diff --git a/docs/src/01_manual/10_theme_customization.md b/docs/src/01_manual/10_theme_customization.md index 59cb5d7..06fccbc 100644 --- a/docs/src/01_manual/10_theme_customization.md +++ b/docs/src/01_manual/10_theme_customization.md @@ -1,3 +1,17 @@ +```@meta +DocTestSetup = quote + using Mousetrap + function Window(app::Application) + out = Mousetrap.Window(app) + set_tick_callback!(out, out) do clock, self + close!(self) + return TICK_CALLBACK_RESULT_DISCONTINUE + end + return out + end +end +``` + # Chapter 10: Theme & Widget Customization In this chapter, we will learn: diff --git a/docs/src/01_manual/11_app_distribution.md b/docs/src/01_manual/11_app_distribution.md index b087bb6..f921ed6 100644 --- a/docs/src/01_manual/11_app_distribution.md +++ b/docs/src/01_manual/11_app_distribution.md @@ -1,3 +1,17 @@ +```@meta +DocTestSetup = quote + using Mousetrap + function Window(app::Application) + out = Mousetrap.Window(app) + set_tick_callback!(out, out) do clock, self + close!(self) + return TICK_CALLBACK_RESULT_DISCONTINUE + end + return out + end +end +``` + # Chapter 11: App Distribution In this chapter, we will learn: diff --git a/docs/src/01_manual/12_opengl_integration.md b/docs/src/01_manual/12_opengl_integration.md index 52c225d..275bebf 100644 --- a/docs/src/01_manual/12_opengl_integration.md +++ b/docs/src/01_manual/12_opengl_integration.md @@ -1,3 +1,17 @@ +```@meta +DocTestSetup = quote + using Mousetrap + function Window(app::Application) + out = Mousetrap.Window(app) + set_tick_callback!(out, out) do clock, self + close!(self) + return TICK_CALLBACK_RESULT_DISCONTINUE + end + return out + end +end +``` + # Chapter 12: OpenGL Integration & Makie Support In this chapter, we will learn: diff --git a/docs/src/02_library/classes.md b/docs/src/02_library/classes.md index 39e0b61..f03738a 100644 --- a/docs/src/02_library/classes.md +++ b/docs/src/02_library/classes.md @@ -708,11 +708,13 @@ ColumnView ColumnViewColumn ``` #### Functions that operate on this type: ++ [`get_expand`](@ref) + [`get_fixed_width`](@ref) + [`get_is_resizable`](@ref) + [`get_is_visible`](@ref) + [`get_title`](@ref) + [`remove_column!`](@ref) ++ [`set_expand!`](@ref) + [`set_fixed_width!`](@ref) + [`set_header_menu!`](@ref) + [`set_is_resizable!`](@ref) diff --git a/docs/src/02_library/functions.md b/docs/src/02_library/functions.md index 2a9023b..57ecb26 100644 --- a/docs/src/02_library/functions.md +++ b/docs/src/02_library/functions.md @@ -1371,6 +1371,10 @@ Mousetrap.get_end_child_resizable ```@docs Mousetrap.get_end_child_shrinkable ``` +## `get_expand` +```@docs +Mousetrap.get_expand +``` ## `get_expand_horizontally` ```@docs Mousetrap.get_expand_horizontally From 9a4a8b23de45e9b364243a477abffb4184aa21db Mon Sep 17 00:00:00 2001 From: Clem Cords Date: Sun, 11 Feb 2024 18:25:40 +0100 Subject: [PATCH 15/31] doctests passing --- docs/src/01_manual/05_event_handling.md | 72 +++++++++++++++++-------- 1 file changed, 49 insertions(+), 23 deletions(-) diff --git a/docs/src/01_manual/05_event_handling.md b/docs/src/01_manual/05_event_handling.md index 187e5bd..b6d61cf 100644 --- a/docs/src/01_manual/05_event_handling.md +++ b/docs/src/01_manual/05_event_handling.md @@ -9,6 +9,9 @@ DocTestSetup = quote end return out end + + app = Mousetrap.Application("test.app") + window = Separator() end ``` @@ -70,9 +73,10 @@ After creating an event controller, it will not yet react to any events. We need We create and connect a `FocusEventController` like so: -```julia +```jldoctest; output = false focus_controller = FocusEventController() add_controller!(window, focus_controller) +# output ``` While the controller will now receive events, nothing else will happen. We need to connect to one or more of its signals, using the familiar signal handler mechanism. @@ -91,7 +95,7 @@ return Mousetrap.@signal_table(FocusEventController, After connecting to these signals: -```julia +```jldoctest; output = false function on_focus_gained(self::FocusEventController) ::Nothing println("focus gained") end @@ -104,6 +108,7 @@ focus_controller = FocusEventController() connect_signal_focus_gained!(on_focus_gained, focus_controller) connect_signal_focus_gained!(on_focus_lost, focus_controller) add_controller!(window, focus_controller) +# output ``` We have successfully created our first event controller. Now, whenever `window` gains focus, a message will be printed. @@ -146,7 +151,7 @@ The signal handler for any of the three signals is handed two arguments, a `KeyC For example, to test whether the user pressed the space key while the shift key is held, we could do: -```julia +```jldoctest; output = false function on_key_pressed(self::KeyEventController, code::KeyCode, modifier_state::ModifierState) ::Nothing if code == KEY_space && shift_pressed(modifier_state) println("shift + space pressed") @@ -157,11 +162,14 @@ key_controller = KeyEventController() connect_signal_key_pressed!(on_key_pressed, key_controller) add_controller!(window, key_controller) +# output ``` While we would connect to the `KeyEventController`s other signals like so: -```julia +```jldoctest; output = false +key_controller = KeyEventController() + function on_key_released(self::KeyEventController, code::KeyCode, modifier_state::ModifierState) ::Nothing # handle key here end @@ -172,6 +180,7 @@ end connect_signal_key_released!(on_key_released, key_controller) connect_signal_modifiers_changed!(on_modifiers_changed, key_controller) +# output ``` `KeyEventController` should be used if we have to monitor both pressing **and releasing** a key or key combination. If all we want to do is trigger behavior when the user presses a combination once, we should use `ShortcutEventController` instead. @@ -184,11 +193,12 @@ To react to the user pressing such a shortcut, we should use [`ShortcutEventCont We first need an `Action`, which we associate a shortcut trigger with: -```julia +```jldoctest shortcut_controller; output = false action = Action("shortcut_controller.example", app) do self::Action println("shift + space pressed") end add_shortcut!(action, "space") +# output ``` Where `app` is an `Application` instance. @@ -197,10 +207,11 @@ We can then create a `ShortcutEventController` instance and call `add_action!`, For the shortcut controller to start receiving events, we also need to connect it to a widget: -```julia +```jldoctest shortcut_controller; output = false shortcut_controller = ShortcutEventController() add_action!(shortcut_controller, action) add_controller!(window, shortcut_controller) +# output ``` Where `window` is the top-level window. Note that `ShortcutEventController` does not have any signals to connect to it. Instead, it automatically listens for shortcuts depending on which `Action` we added. @@ -231,7 +242,7 @@ return Mousetrap.@signal_table(MotionEventController, Shown here is an example of how to connect to these signals, where we forwarded `window`, the host widget of the controller, as the `data` argument of signal `motion`, to calculate the absolute position of the cursor on screen. -```julia +```jldoctest motion_controller; output = false function on_motion(::MotionEventController, x::AbstractFloat, y::AbstractFloat, data::Widget) ::Nothing widget_position = get_position(data) cursor_position = Vector2f(x, y) @@ -239,28 +250,32 @@ function on_motion(::MotionEventController, x::AbstractFloat, y::AbstractFloat, println("Absolute Cursor Position: $(widget_position + cursor_position)") end -window = Window(app) motion_controller = MotionEventController() - connect_signal_motion!(on_motion, motion_controller, window) add_controller!(window, motion_controller) +# output ``` While we would connect to `MotionEventController`s other signals like so: -```julia -function on_motion_enter(::MotionEventController, x::AbstractFloat, y::AbstractFloat) ::Nothing +```jldoctest motion_controller; output = false +function on_motion_enter(::MotionEventController, x::AbstractFloat, y::AbstractFloat, data::Widget) ::Nothing # handle cursor enter + return nothing end -function on_motion_leave(::MotionEventController) ::Nothing +function on_motion_leave(::MotionEventController, data::Widget) ::Nothing # handle cursor leave + return nothing end connect_signal_motion_enter!(on_motion_enter, motion_controller, window) connect_signal_motion_leave!(on_motion_leave, motion_controller, window) +# output ``` +Where `window` was passed as the data argument of `connect_signal_` such that is available from within the signal handler. + --- ## Mouse Button Presses: ClickEventController @@ -312,7 +327,7 @@ If we only want signals to be emitted for certain buttons, we can use [`set_only As an example, if we want to check if the user pressed the left mouse button twice, we can do the following: -```julia +```jldoctest click_controller; output = false function on_click_pressed(self::ClickEventController, n_presses::Integer, x::AbstractFloat, y::AbstractFloat) ::Nothing if n_presses == 2 && get_current_button(self) == BUTTON_ID_BUTTON_01 println("double click registered at ($x, $y)") @@ -322,11 +337,12 @@ end click_controller = ClickEventController() connect_signal_click_pressed!(on_click_pressed, click_controller) add_controller!(window, click_controller) +# output ``` While we would connect to `ClickEventController`s other signals like so: -```julia +```jldoctest click_controller; output = false function on_click_released(self::ClickEventController, n_presses::Integer, x::AbstractFloat, y::AbstractFloat) ::Nothing # handle button up end @@ -337,6 +353,7 @@ end connect_signal_click_released!(on_click_released, click_controller) connect_signal_click_stopped!(on_click_stopped, click_controller) +# output ``` While `ClickEventController` gives us full control over one or more clicks, there is a more specialized @@ -362,7 +379,7 @@ Similar to `clicked`, `LongPressEventController` provides us with the location o `LongPressEventController`, like `ClickEventController`, subtypes `SingleClickGesture`, which allows us to differentiate between different mouse buttons or a touchscreen, just as before. -```julia +```jldoctest; output = false function on_pressed(self::LongPressEventController, x::AbstractFloat, y::AbstractFloat) ::Nothing println("long press registered at ($x, $y)") end @@ -375,6 +392,7 @@ long_press_controller = LongPressEventController() connect_signal_pressed!(on_pressed, long_press_controller) connect_signal_press_cancelled!(on_press_cancelled, long_press_controller) add_controller!(window, long_press_controller) +# output ``` --- @@ -405,7 +423,7 @@ To get the current position of the cursor, we have to add the offset from `scrol To track the cursor position during a drag gesture, we can connect to `DragEventController`s signals like so: -```julia +```jldoctest; output = false function on_drag_begin(self::DragEventController, start_x::AbstractFloat, start_y::AbstractFloat) ::Nothing println("drag start: ($start_x, $start_y)") end @@ -426,6 +444,7 @@ connect_signal_drag!(on_drag, drag_controller) connect_signal_drag_end!(on_drag_end, drag_controller) add_controller!(window, drag_controller) +# output ``` --- @@ -452,7 +471,7 @@ Which is emitted once per frame while the gesture is active. The second argument is the current offset, that is, the distance between the current position of the cursor and the position at which the gesture was first recognized, relative to the host widget's origin, in pixels. -```julia +```jldoctest; output = false function on_pan(self::PanEventController, direction::PanDirection, offset::AbstractFloat) ::Nothing if direction == PAN_DIRECTION_LEFT println("panning left by $offset") @@ -464,6 +483,7 @@ end pan_controller = PanEventController(ORIENTATION_HORIZONTAL) connect_signal_pan!(on_pan, pan_controller) add_controller!(window, pan_controller) +# output ``` --- @@ -490,7 +510,7 @@ When the user stops scrolling, `scroll_end` is emitted once. If we want to keep track of how far the user has scrolled a widget that had a `ScrollEventController` connect, we do the following: -```julia +```jldoctest scroll_controller; output = false # variable to keep track of distance scrolled distance_scrolled = Ref{Vector2f}(Vector2f(0, 0)) @@ -518,6 +538,7 @@ connect_signal_scroll!(on_scroll, scroll_controller) connect_signal_scroll_end!(on_scroll_end, scroll_controller) add_controller!(window, scroll_controller) +# output ``` Where we used a global [`Ref`](https://docs.julialang.org/en/v1/base/c/#Core.Ref) to safely reference the value of `distance_scrolled` from within the signal handlers. @@ -528,7 +549,7 @@ Where we used a global [`Ref`](https://docs.julialang.org/en/v1/base/c/#Core.Ref To allow for kinetic scrolling, we need to enable it using [`set_kinetic_scrolling_enabled!`](@ref), then connect to the appropriate signal: -```julia +```jldoctest scroll_controller; output = false # enable kinetic scrolling set_kinetic_scrolling_enabled!(scroll_controller, true) @@ -539,6 +560,7 @@ end # connect handler connect_signal_kinetic_scroll_decelerate!(on_kinetic_scroll_decelerate, scroll_controller) +# output ``` --- @@ -562,7 +584,7 @@ The argument `scale` is a *relative* scale, where `1` means no change between th To detect whether a user is currently zooming out (pinching) or zooming in, we could do the following: -```julia +```jldoctest; output = false function on_scale_changed(self::PinchZoomEventController, scale::AbstractFloat) ::Nothing if scale < 1 println("zooming in") @@ -574,6 +596,7 @@ end zoom_controller = PinchZoomEventController() connect_signal_scale_changed!(on_scale_changed, zoom_controller) add_controller!(window, zoom_controller) +# output ``` --- @@ -593,7 +616,7 @@ return Mousetrap.@signal_table(RotateEventController, It takes two arguments: `angle_absolute` and `angle_delta`. `angle_absolute` provides the current angle between the two fingers. `angle_delta` is the difference between the current angle and the angle at the start of the gesture. Both `angle_absolute` and `angle_delta` are provided in radians, to convert them we can use [`Mousetrap.Angle`](@ref): -```julia +```jldoctest; output = false function on_rotation_changed(self::RotateEventController, angle_delta, angle_absolute) ::Nothing # convert to unit-agnostic Mousetrap.Angle absolute = radians(angle_absolute) @@ -605,6 +628,7 @@ end rotation_controller = RotateEventController() connect_signal_rotation_changed!(on_rotation_changed, rotation_controller) add_controller!(window, rotation_controller) +# output ``` --- @@ -626,7 +650,7 @@ The signal handler provides two arguments, `x_velocity` and `y_velocity`, which To illustrate how to deduce the direction of the swipe, consider this example: -```julia +```jldoctest; output = false function on_swipe(self::SwipeEventController, x_velocity::AbstractFloat, y_velocity::AbstractFloat) ::Nothing print("swiping ") if (y_velocity < 0) @@ -645,6 +669,7 @@ end swipe_controller = SwipeEventController() connect_signal_swipe!(on_swipe, swipe_controller) add_controller!(window, swipe_controller) +# output ``` UIs should react to both the direction and magnitude of the vector, even though the latter is ignored in this example. @@ -675,7 +700,7 @@ We recognize signal `motion` from `MotionEventController`. It behaves [exactly t The three other signals are used to react to the physical distance between the stylus and touchpad. `stylus_down` is emitted when the pen's tip makes contact with the touchpad, `stylus_up` is emitted when this contact is broken, `proximity` is emitted when the stylus is about to touch the touchpad, or just left the touchpad. -```julia +```jldoctest; output = false function on_stylus_up(self::StylusEventController, x::AbstractFloat, y::AbstractFloat) ::Nothing println("stylus is no longer touching touchpad, position: ($x, $y)") end @@ -699,6 +724,7 @@ connect_signal_proximity!(on_proximity, stylus_controller) connect_signal_motion!(on_motion, stylus_controller) add_controller!(window, stylus_controller) +# output ``` ### Stylus Axis From 6758ee59a97c8c59c379f8cbd8ddbf09a1121367 Mon Sep 17 00:00:00 2001 From: Clem Cords Date: Sun, 11 Feb 2024 18:27:32 +0100 Subject: [PATCH 16/31] typo --- src/Mousetrap.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Mousetrap.jl b/src/Mousetrap.jl index 011fe56..95bdff9 100644 --- a/src/Mousetrap.jl +++ b/src/Mousetrap.jl @@ -1757,7 +1757,7 @@ module Mousetrap export RGBA function RGBA(r::AbstractFloat, g::AbstractFloat, b::AbstractFloat, a::AbstractFloat) - return RBGA( + return RGBA( convert(Cfloat, r), convert(Cfloat, g), convert(Cfloat, b), From 0695e7d48103033be4287d8e7a26363fb1a84b52 Mon Sep 17 00:00:00 2001 From: Clem Cords Date: Sun, 11 Feb 2024 18:37:52 +0100 Subject: [PATCH 17/31] added doctests --- docs/src/01_manual/06_image.md | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/docs/src/01_manual/06_image.md b/docs/src/01_manual/06_image.md index 1c87c5d..db97d4e 100644 --- a/docs/src/01_manual/06_image.md +++ b/docs/src/01_manual/06_image.md @@ -49,20 +49,23 @@ For both representations, all components are 32-bit floats in `[0, 1]`. The **al We can freely convert between `RGBA` and `HSVA`. To do this, we use [`rgba_to_hsva`](@ref) and [`hsva_to_rgba`](@ref): -```julia +```jldoctest; output = false rgba = RGBA(0.1, 0.2, 0.3, 0.4) as_hsva = rgba_to_hsva(rgba) as_rgba = hsva_to_rgba(as_hsva) -@assert rgba == as_rgba # true +@assert isapprox(rgba, as_rgba) # true +# output ``` +Note the use of `isapprox`, which can also be written as `rgba ≈ as_rgba`. Conversion can introduce floating point error, but their precision will be such that for the actual color on screen, a difference is not noticeable. If we directly compare the floating point components, the above assertion would fail. + ### Color to Hexadecimal Mousetrap offers a function to convert `RGBA` to its HTML color code. This code is a a string of the form `#RRGGBB`, where `RR` is the red, `GG` the green, and `BB` the blue component, in unsigned 8-bit hexadecimal. For example, the color `RGBA(1, 0, 1, 1)` would have the HTML-code `#FF00FF`, where the alpha component was omitted. Using `html_code_to_rgba` and `rgba_to_html_code`, we can freely convert between a colors in-memory and hexadecimal representation. For example, if we want to use an `Entry` for the user to be able to enter a color as an HTML color code, we could do the following: -```julia +```jldoctest; output = false entry = Entry() connect_signal_activate!(entry) do self::Entry text = get_text(self) @@ -74,6 +77,7 @@ connect_signal_activate!(entry) do self::Entry end return nothing end +# output ``` If parsing was successful, `is_valid_html_code` will return `true`, at which point we can be sure that `html_code_to_rgba` will return a valid color. @@ -113,7 +117,7 @@ The function called when the dialog is dismissed is registered using `on_cancel! We would use these two functions like so: -```julia +```jldoctest; output = false color_chooser = ColorChooser("Choose Color") # react to user selection @@ -127,6 +131,7 @@ on_cancel!(color_chooser) do self::ColorChooser end present!(color_chooser) +# output ``` At any point, we can also access the last selected color by calling [`get_color`](@ref) on the `ColorChooser` instance. From 2a4ddb1965b9323f384465f4e0950432c562527b Mon Sep 17 00:00:00 2001 From: Clem Cords Date: Sun, 11 Feb 2024 18:37:59 +0100 Subject: [PATCH 18/31] added isapprox for colors --- src/Mousetrap.jl | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Mousetrap.jl b/src/Mousetrap.jl index 95bdff9..9dffbf2 100644 --- a/src/Mousetrap.jl +++ b/src/Mousetrap.jl @@ -1782,6 +1782,9 @@ module Mousetrap ) end + Base.isapprox(x::RGBA, y::RGBA) = isapprox(x.r, y.r) && isapprox(x.g, y.g) && isapprox(x.b, y.b) && isapprox(x.a, y.a) + Base.isapprox(x::HSVA, y::HSVA) = isapprox(x.h, y.h) && isapprox(x.s, y.s) && isapprox(x.v, y.v) && isapprox(x.a, y.a) + import Base.== ==(x::RGBA, y::RGBA) = x.r == y.r && x.g == y.g && x.b == y.b && x.a == y.a function ==(x::HSVA, y::HSVA) From 0f9dd72d473e4ac1146949a2b46b8daeee2bfb88 Mon Sep 17 00:00:00 2001 From: Clem Cords Date: Sun, 11 Feb 2024 18:49:09 +0100 Subject: [PATCH 19/31] added doctests --- docs/src/01_manual/07_os_interface.md | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/docs/src/01_manual/07_os_interface.md b/docs/src/01_manual/07_os_interface.md index 1b90100..631a1ca 100644 --- a/docs/src/01_manual/07_os_interface.md +++ b/docs/src/01_manual/07_os_interface.md @@ -9,6 +9,8 @@ DocTestSetup = quote end return out end + + widget = Separator() end ``` @@ -169,7 +171,7 @@ As mentioned before, messages of level `DEBUG` are only printed if we specifical For example, if our log domain is `foo`: -```julia +```@repl # define custom domain const FOO_DOMAIN = "foo" @@ -193,7 +195,7 @@ Regardless of OS, we can forward all logging, including that of Mousetrap itself When stored to a file, logging messages will have a different format that may or may not list additional information when compared to logging to a console. The philosophy behind this is that it is better to log as much information as possible, and then use second-party software to filter it, as opposed to missing crucial information for the sake of brevity: -```cpp +```julia const LogDomain FOO_DOMAIN = "foo" if !set_log_file(FOO_DOMAIN, "example_log.txt") log_fatal(FOO_DOMAIN, "In set_log_file: Unable to create file at `example_log.txt`") @@ -233,7 +235,7 @@ When querying information about a file, we use [`FileDescriptor`](@ref), which r We can create a file descriptor from a path like so: -```cpp +```julia readonly = FileDescriptor() create_from_path!(readonly, "/home/user/Desktop/example.txt"); ``` @@ -353,7 +355,7 @@ The following monitor events are supported: For example, if we want to trigger an action whenever `/path/to/file.txt` changes its content, we could do the following: -```julia +```jldoctest; output = false to_watch = FileDescriptor("/path/to/file.txt") # equivalent to create_from_path! monitor = create_monitor(to_watch) @@ -363,6 +365,7 @@ function on_file_changed_callback(monitor::FileMonitor, event::FileMonitorEvent, end end on_file_changed!(on_file_changed_callback, monitor) +# output ``` If we no longer want to monitor a file, we can call [`cancel!`](@ref), at which point the `FileMonitor` instance may be safely deallocated. @@ -537,7 +540,7 @@ Mousetrap offers a less intrusive way of showing a message to a user, facilitate ![](../assets/popup_message.png) !!! details "How to generate this image" - ```julia + ```jldoctest; output = false using Mousetrap main() do app::Application window = Window(app) @@ -562,6 +565,8 @@ Mousetrap offers a less intrusive way of showing a message to a user, facilitate set_child!(window, message_overlay) present!(window) end + # output + 0 ``` To send a message, we first need to instance an object of type [`PopupMessage`](@ref), which, unlike `PopupMessageOverlay`, is **not** a widget, though it is a `SignalEmitter`. @@ -570,12 +575,13 @@ A popup message always has a title, which we supply to its constructor. It will After instancing the `PopupMessage`, we present it to the user using [`show_message!`](@ref): -```julia +```jldoctest popup_message; output = false message_overlay = PopupMessageOverlay() set_child!(message_overlay, widget) popup_message = PopupMessage("Message Text") show_message!(message_overlay, popup_message) +# output ``` We can set the message to hide itself automatically by setting [`set_timeout!`](@ref) to anything other than `0`. Only one message can be shown at a time. We can make sure an important message is jumped to the front of the queue by setting its priority using [` set_is_high_priority!`](@ref). @@ -586,7 +592,7 @@ A `PopupMessage` has one optional button. To show this button, we need to choose For example, to trigger a function when the user clicks the "OK" button of our `PopupMessage`, we could do: -```julia +```jldoctest popup_message; output = false popup_message = PopupMessage("Message Text") # connect to button press @@ -596,6 +602,7 @@ connect_signal_button_clicked!(popup_message) do self::PopupMessage end show_message!(message_overlay, popup_message) +# output ``` ## Popup Messages @@ -605,7 +612,7 @@ Mousetrap offers a less intrusive way of showing a message to a user, facilitate ![](../assets/popup_message.png) !!! details "How to generate this image" - ```julia + ```jldoctest; output = false using Mousetrap main() do app::Application window = Window(app) @@ -630,6 +637,8 @@ Mousetrap offers a less intrusive way of showing a message to a user, facilitate set_child!(window, message_overlay) present!(window) end + # output + 0 ``` To send a message, we first need to instance an object of type [`PopupMessage`](@ref), which, unlike `PopupMessageOverlay`, is **not** a widget, though it is a `SignalEmitter`. From c04035ee847e63f1c402ee5d334b0aafaa0936e0 Mon Sep 17 00:00:00 2001 From: Clem Cords Date: Sun, 11 Feb 2024 19:16:48 +0100 Subject: [PATCH 20/31] doctests working --- docs/src/01_manual/08_menus.md | 331 ---------------------- docs/src/01_manual/09_native_rendering.md | 112 +++++--- 2 files changed, 76 insertions(+), 367 deletions(-) delete mode 100644 docs/src/01_manual/08_menus.md diff --git a/docs/src/01_manual/08_menus.md b/docs/src/01_manual/08_menus.md deleted file mode 100644 index 67b54ba..0000000 --- a/docs/src/01_manual/08_menus.md +++ /dev/null @@ -1,331 +0,0 @@ -```@meta -DocTestSetup = quote - using Mousetrap - function Window(app::Application) - out = Mousetrap.Window(app) - set_tick_callback!(out, out) do clock, self - close!(self) - return TICK_CALLBACK_RESULT_DISCONTINUE - end - return out - end -end -``` - -# Chapter 8: Menus - -In this chapter, we will learn: -+ How to create complex, nested menus -+ How to display menus using `PopoverMenu` and `MenuBar` -+ Best-practice style guides for menus - ---- - -In the [chapter on actions](./03_actions.md) we learned that we can trigger an action using [`Button`](@ref), by assigning an action to it using [`set_action!`](@ref). -This works if we want to have a GUI element that has one or maybe a few actions. In practice, an application can have hundreds of different actions. Asking users to trigger these using an equal number of buttons would be unsustainable. For situations like these, we should instead turn to **menus**. - -## MenuModel Architecture - -In Mousetrap, menus follow the [model-view architectural pattern](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller). To create a menu, we need -the menu model, which holds information about how the menu is structured, along with a view, which takes the models and transforms them into an interactable -widget users can manipulate. Changing the model changes the view. - -!!! details "Running Snippets from this Section" - - To follow along with code snippets from this section, we can use the following `main.jl` file: - ```julia - using Mousetrap - main() do app::Application - - window = Window(app) - - # create dummy action - action = Action("dummy.action", app) do x - println("triggered.") - end - - # model that we will be modifying in the snippet - root = MenuModel() - - # snippet goes here - - # display menu - add_submenu!(root, "Title", model) - view = PopoverButton(PopoverMenu(root)) - set_child!(window, view) - present!(window) - end - ``` - -## Menu Items - -[`MenuModel`](@ref), the model component of Mousetrap menus, is a list of **menu items** in a specific order. If item `A` is added before item `B` at runtime, then `A` will appear above item `B`. There are multiple different types of menu items, all with their own purpose. The function we choose to add an item to the model determines the item's type. There are four types of menu items, which we will go over in this section. - -### Item Type #1: Action - -The first and most simple item type is called "action". Added via [`add_action!`](@ref), which takes both a label and an [`Action`](@ref) instance, this item is a simple button with text. If the user clicks the button, the action is executed. - -Similar to `Button`, if the action is disabled (via [`set_enabled!`](@ref)) or does not yet have a callback registered, the menu item will appear "greyed out" and cannot be activated. - -```cpp -add_action!(model, "Action Item #1", action) -add_action!(model, "Action item #2", action) -add_action!(model, "Action item #3", action) -``` - -![](../assets/menu_model_actions.png) - -### Item Type #2: Widgets - -Secondly, we have perhaps the most powerful type of item: A custom widget. We add an item of this type using [`add_widget!`](@ref), which only takes a single argument, the widget itself. This widget can be arbitrarily complex, though it is usually not advisable to insert an entire `ColumnView` into a tiny menu. Good-practice examples include `Button`, `Entry`, `CheckButton`, `ToggleButton`, `Switch`, and `SpinButton`, all of which are intractable. - -We do not supply an `Action` instance with this item, if we want the user interacting with the menu item to trigger behavior, we will have to connect that behavior to the signals of the widget we inserted into the menu, or any of its event controllers. - -```cpp -add_widget!(model, hbox(Label("Choose Value: "), SpinButton(0, 1, 0.01))) -add_widget!(model, Scale(0, 1, 0.01, ORIENTATION_HORIZONTAL)) -add_widget!(model, hbox(Label("Enter Text: "), Entry())) -``` - -![](../assets/menu_model_widgets.png) - -Widgets are the most flexible type of menu item. They should be used with caution, and only if absolutely necessary. It is often better to create an action that opens a separate [`Window`](@ref) that contains the widget, as opposed to directly adding the widget to the menu. - -### Item Type #3: Submenu - -`MenuModel` can be **nested**, which means we can insert a `MenuModel` into another `MenuModel`. This is similar to `ListView` or file-system tree: a folder can contain regular files (menu items), or it can contain other folders (menu models), which in turn can also contain another file or folder, etc. `MenuModel` can similarly be infinitely nested, though it is usually not recommended to go deeper than two or three levels. - -We call a `MenuModel` that is nested within another model a **submenu**. It will show up as a button with a label, along with an indicator that it is a submenu, usually a `>` shape. Clicking this item will reveal the submenu. - -To add this type of submenu item, we use [`add_submenu!`](@ref), which takes a title and another menu model: - -```julia -submenu_01 = MenuModel() -add_widget!(submenu_01, submenu_content()) -add_submenu!(model, "Submenu #1", submenu_01) - -submenu_02 = MenuModel() -add_widget!(submenu_02, submenu_content()) -add_submenu!(model, "Submenu #2", submenu_02) -``` -![](../assets/menu_model_submenu_outer.png) - -Clicking on of these items will reveal the submenu content: - -![](../assets/menu_model_submenu_inner.png) - -Where `submenu_content()` returns a simple place-holder widget, in reality, this will be many other menu items or other submenus. - -### Item Type #4: Icons - -A prettier analog to the "action"-type menu item, an "icon"-type item. Added via [`add_icon!`](@ref), it takes an [`Icon`](@ref), along with an action. The entire menu item will be just the icon, it may not have any text. If the icon is clicked, the action is executed. - -```julia -add_icon!(model, Icon(#=...=#), action) -add_icon!(model, Icon(#=...=#), action) -add_icon!(model, Icon(#=...=#), action) -add_icon!(model, Icon(#=...=#), action) -``` -![](../assets/menu_model_icons.png) - -Where we used the default Gnome icons for weather indicators as placeholders. - -When creating a menu item with an action, we have to decide whether to use a simple text label or an icon. We may not have both. - -### Item Type #5: Sections - -Lastly, we have **sections**. Sections are like submenus, in that they are a menu inside another menu. The difference is in the way this inner menu is displayed. - -When adding a submenu with `add_submenu!`, a single new item will appear in the outer menu. When clicked, the menu "slides" to reveal the submenu. - -With sections, the inner menu is instead inserted directly into the outer menu; both are shown at the same time. - -We add a "section"-type item using [`add_section!`](@ref), which takes another menu model and a title, which will be used as a header for the section: - -```julia -section = MenuModel() -add_action!(section, "Section Item #01", #= action =#) -add_action!(section, "Section Item #02", #= action =#) -add_action!(section, "Section Item #03", #= action =#) -add_section!(model, "Section Label", section) -``` -![](../assets/menu_model_section.png) - - -We see that the section label, `"Section Label"` in this case, is displayed above all its items, which are inserted into the outer menu. In this way, sections can be helpful to group multiple menu items, which makes a menu easier to parse without adding another nested level via a submenu. - -#### Section Formats - -[`add_section!`](@ref) takes one additional, optional argument, which is a [`SectionFormat`](@ref). This enum has a number of values that govern how the section is displayed: - -+ `SECTION_FORMAT_CIRCULAR_BUTTONS` displays all its elements in circular buttons -+ `SECTION_FORMAT_HORIZONTAL_BUTTONS`: display its elements as a row of rectangular buttons -+ `SECTION_FORMAT_INLINE_BUTTONS`: display all buttons right of the section heading - -The following shows these section formats: - -```julia -# generate a menu model with the 4 weather icons, then add as section with given format -function add_icon_section(title::String, format::SectionFormat) - - section = MenuModel() - add_icon!(section, Icon(#=...=#), action) - add_icon!(section, Icon(#=...=#), action) - add_icon!(section, Icon(#=...=#), action) - add_icon!(section, Icon(#=...=#), action) - add_section!(icon_model, title, section, format) -end - -add_icon_section("Normal", SECTION_FORMAT_NORMAL) -add_icon_section("Horizontal Buttons", SECTION_FORMAT_HORIZONTAL_BUTTONS) -add_icon_section("Inline Buttons: ", SECTION_FORMAT_INLINE_BUTTONS) -add_icon_section("Circular Buttons", SECTION_FORMAT_CIRCULAR_BUTTONS) -``` -![](../assets/menu_model_section_formats.png) - -Using `SectionFormat` and mixing several menu item types, we can construct arbitrarily complex menus. We should realize that the highest priority when constructing menu items is the **user experience**. Presenting users with a giant, deeply nested mess of buttons may be very functional, but it may not be very usable to anyone but the developers themself. - ---- - -## Displaying Menus - -Now that we have learned to construct the menu **model**, we should turn our attention to the **view**, a widget displaying a `MenuModel`. - -### PopoverMenu - -[`PopoverMenu`](@ref) is a sibling of [`Popover`](@ref). It consists of a small dialog that is only displayed when we ask it to. While `Popover` displays an arbitrary widget, `PopoverMenu` displays a menu model. - -For ease of use, it's easiest to connect the `PopoverMenu` to a [`PopoverButton`](@ref), just like we did with `Popover`: - -```julia -model = MenuModel() - -# fill `model` here - -popover_menu = PopoverMenu(model) -popover_button = PopoverButton(popover_menu) - -# add the button to the window so we can click it -set_child!(window, popover_button) -``` - -The `PopoverMenu`-`PopoverButton` combo should be reserved for **context menus**, which are menus that act on some local part of the application. For a menu that affects the entire application and should be accessible at all times, we should use the next menu model view instead. - -### MenuBar - -Familiar to any user of a modern desktop GUI, [`MenuBar`](@ref) is a widget that is usually displayed at the top of the main application window: - -![](../assets/menu_bar.png) - -We see that `MenuBar` is a horizontal list of items. When the user clicks on one of the items, a nested menu opens. Just like before, menus can be nested a theoretically infinite number of times. - -Unlike `PopoverMenu`, `MenuBar` requires its `MenuModel` to have a certain structure: **all top-level items have to be submenus**. - -What does this mean? Let's work through the menu shown in the image above. We created it using the following snippet: - -```julia -root = MenuModel() - -file_submenu = MenuModel() -add_action!(file_submenu, "Open", #= action =#) - -file_recent_submenu = MenuModel() -add_action!(file_recent_submenu, "Project 01", #= action =#) -add_action!(file_recent_submenu, "Project 02", #= action =#) -add_action!(file_recent_submenu, "Other...", #= action =#) -add_submenu!(file_submenu, "Recent...", file_recent_submenu) - -add_action!(file_submenu, "Save", #= action =#) -add_action!(file_submenu, "Save As...", #= action =#) -add_action!(file_submenu, "Exit", #= action =#) - -help_submenu = MenuModel() -add_submenu!(root, "File", file_submenu) -add_submenu!(root, "Help", help_submenu) - -menubar = MenuBar(model) -``` - -Where in a real application, each item will have a different action. - -This code can be quite hard to read. To make the nesting easier to understand, we'll write it out as if it were a file system folder structure: - -``` -model \ - File \ - Open - Recent... \ - Project 01 - Project 02 - Other... - Save - Save As - Exit - Help \ - (...) -``` - -Where any line suffixed with a `\` is a submenu. - -We see that we have four models in total. The top-most menu model is called `root`, it is what `MenuBar` will be initialized with. -Next, we have the model called `file_submenu`, which has the title `File`. It contains five items, titled `Open`, `Recent...`, `Save`, `Save As`, and `Exit`. `Recent...` is a submenu-type item, created from a `MenuModel` called `file_recent_submenu` in the above code. This model, in turn, has three items. On the same level as `File`, we have a second menu `Help`. - -The **top-level** menu is `root`. It is used as the argument for the constructor of `MenuBar`. We see that all direct children of `root` (`File` and `Help`) **are themselves submenus** (they were added using `add_submenu!`). - -No direct child of `root` is an "action"-, "widget"-, "icon"- or "section"-type item. This is what is required for `MenuBar`. All top-level items have to be submenus. - -!!! warning - Due to a bug in the backend, as of `v0.3.0`, a menu model used for a `MenuBar` **may not have a "widget"-type item in a submenu of a submenu**. - - This means we *can* add a widget to any submenu of `root`, but we may not add - a widget to any submenu that is nested any deeper than a direct child of `root`. - - This bug does not affect `PopoverMenu`, for whom we can put a widget at any - depth. `PopoverMenu` has no requirement as to the structure of its menu model, while `MenuBar` requires that all top-level items are submenus and that no submenu of a submenu may have a "widget"-type item. - -## Main Menu (macOS only) - -!!! compat - Features from this section are only available with Mousetrap `v0.3.1` or newer, and should only be used by applications targeting macOS. We can use `Sys.isapple` to verify the user's operating system. - -On macOS, applications are able to target the [**main menu**](https://support.apple.com/en-gb/guide/mac-help/mchlp1446/mac), which is the menubar at the very top of the screen (outside the Mousetrap window). This bar contains the Apple menu, as well as a native menubar with application-specific options. To bind a `Mousetrap.MenuModel` to this menubar, we use `set_menubar` on our `Application` instance: - -```julia -main() do app::Application - - model = MenuModel() - # fill model here - - if Sys.isapple() - set_menubar(app, model) # associate model with the Apple main menu - end -end -``` - -Note that it may be necessary to call `set_menubar` again when the `MenuModel` is modified in order for the main menu to be updated. - ---- - -## Style End Note - -Menus are extremely powerful and complex to construct. With practice and good software / UI design, we can create deep, complex menus that are still easy to understand and use. We, as developers, should make this our priority. - -Some additional notes: - -### Ellipses - -Some may be curious as to why some menu items have `...` added at the end of their labels, while others do not. This is not a universal standard, but it is common for `...` to indicate that clicking this item will open another window, submenu, or dialog. If clicking an item simply triggers an action (such as `Save` or `Exit`), `...` is omitted. If the item opens a window, widget, or submenu, `...` is appended to the menu label, as is the case with `Recent...` above. - -### Maximum Menu Depth - -Regarding menu depth, the best practice is to never go deeper than three levels. The above example with `File > Recent... > Project 01` shows a good situation in which a 3-level-deep menu may be justified. Going any deeper is rarely a good course of action. Adding a section should always be considered *before* deciding to add a submenu. - -### Section Grouping - -Lastly, some schools of UI design believe that **every menu item should be inside a section**. For example, if we were to follow this philosophy for our above `MenuBar` example, we would redesign it like this: - -![](../assets/menu_bar_with_sections.png) - -This adds considerable complexity to the code (adding 4 models, one for each section, making our total 8). In return, items are grouped logically, and each item gets a "heading", which helps make long menus easier to understand. For this small example, this is most likely unnecessary, but it will be more attractive for a menubar with dozens of items. - -In the end, each UI designer should decide for themselves which technique to use. What all should agree on, however, is that ease of use for the end-user is the most important thing. It should trump ease of development in every case. If something is harder for us developers but makes it easier for our users, we should go through the effort of doing it. diff --git a/docs/src/01_manual/09_native_rendering.md b/docs/src/01_manual/09_native_rendering.md index 84335d6..e55933f 100644 --- a/docs/src/01_manual/09_native_rendering.md +++ b/docs/src/01_manual/09_native_rendering.md @@ -9,6 +9,8 @@ DocTestSetup = quote end return out end + + shape = rectangle = Rectangle(Vector2f(-0.5, 0.5), Vector2f(1, 1)) end ``` @@ -118,7 +120,7 @@ At any point, we can convert between the coordinate systems using [`from_gl_coor We'll now create our first shape, which is a point. A point is always exactly one pixel in size. -```julia +```@repl shape = Shape() as_point!(shape, Vector2f(0, 0)) ``` @@ -127,7 +129,7 @@ Where we use `Vector2f(0, 0)` as the point's position, meaning it will appear at The above is directly equivalent to the following: -```julia +```@repl shape = Point(Vector2f(0, 0)) ``` @@ -152,7 +154,7 @@ add_render_task!(render_area, RenderTask(shape)) ![](../assets/shape_hello_world.png) !!! details "How to generate this Image" - ```julia + ```jldoctest; output = false using Mousetrap main() do app::Application @@ -171,6 +173,8 @@ add_render_task!(render_area, RenderTask(shape)) set_child!(window, frame) present!(window) end + # output + 0 ``` If we want to remove a task from our render area, we need to call [`clear_render_tasks!`](@ref), then add all other render tasks again. @@ -183,8 +187,10 @@ Mousetrap offers a wide variety of pre-defined shape types. Thanks to this, we d As we've seen, [`Point`](@ref) is always exactly one pixel in size. Its constructor takes a single `Vector2f`: -```julia +```jldoctest; output = false point = Point(Vector2f(0, 0)) +# output +Shape(native_handle = 0) ``` ![](../assets/shape_point.png) @@ -193,12 +199,14 @@ point = Point(Vector2f(0, 0)) [`Points`](@ref) is a number of points. Instead of taking a single `Vector2f`, its constructor takes `Vector{Vector2f}`: -```julia +```jldoctest; output = false points = Points([ Vector2f(-0.5, 0.5), Vector2f(0.5, 0.5), Vector2f(0.0, -0.5) ]) +# output +Shape(native_handle = 0) ``` ![](../assets/shape_points.png) @@ -209,11 +217,13 @@ Rendering several points using `Points` is much more performant, a four-vertex ` A [`Line`](@ref) is defined by two points, between which a 1-pixel thick line will be drawn: -```julia +```jldoctest; output = false line = Line( Vector2f(-0.5, +0.5), Vector2f(+0.5, -0.5) ) +# output +Shape(native_handle = 0) ``` ![](../assets/shape_line.png) @@ -222,11 +232,13 @@ line = Line( [`Lines`](@ref) will draw unconnected lines. It takes a vector of point pairs. For each of these, a 1-pixel thick line will be drawn between them. -```julia +```jldoctest; output = false lines = Lines([ Vector2f(-0.5, 0.5) => Vector2f(0.5, -0.5), Vector2f(-0.5, -0.5) => Vector2f(0.5, 0.5) ]) +# output +Shape(native_handle = 0) ``` ![](../assets/shape_lines.png) @@ -237,13 +249,15 @@ lines = Lines([ A line will be drawn between each successive pair of coordinates, meaning the last vertex of the previous line will be used as the first vertex of the current line. If the supplied vector of points is `{a1, a2, a3, ..., a(n)}` then `LineStrip` will render as a series of lines with coordinate pairs `{a1, a2}, {a2, a3}, ..., {a(n-1), a(n)}` -```julia +```jldoctest; output = false line_strip = LineStrip([ Vector2f(-0.5, +0.5), Vector2f(+0.5, +0.5), Vector2f(+0.5, -0.5), Vector2f(-0.5, -0.5) ]) +# output +Shape(native_handle = 0) ``` ![](../assets/shape_line_strip.png) @@ -252,13 +266,15 @@ line_strip = LineStrip([ [`Wireframe`](@ref) is similar to a `LineStrip`, except that it also connects the last and first vertex. For a supplied vector of points `{a1, a2, a3, ..., an}`, the series of lines will be `{a1, a2}, {a2, a3}, ..., {a(n-1), a(n)}, {a(n), a1}`, the last vertex-coordinate pair is what distinguishes it from a `LineStrip`. As such, `Wireframe` is sometimes also called a **line loop**. -```julia +```jldoctest; output = false wireframe = Wireframe([ Vector2f(-0.5, +0.5), Vector2f(+0.5, +0.5), Vector2f(+0.5, -0.5), Vector2f(-0.5, -0.5) ]) +# output +Shape(native_handle = 0) ``` ![](../assets/shape_wireframe.png) @@ -269,12 +285,14 @@ Note how this shape takes the same coordinates as `LineStrip`, but draws one mor A [`Triangle`](@ref) is constructed as one would expect, using three `Vector2f`, one for each of its vertices: -```julia +```jldoctest; output = false triangle = Triangle( Vector2f(-0.5, 0.5), Vector2f(+0.5, 0.5), Vector2f(0.0, -0.5) ) +# output +Shape(native_handle = 0) ``` ![](../assets/shape_triangle.png) @@ -283,11 +301,13 @@ triangle = Triangle( A [`Rectangle`](@ref) has four vertices. It is defined by its top-left point and its width and height. As such, it is always axis-aligned. -```julia +```jldoctest; output = false rectangle = Rectangle( Vector2f(-0.5, 0.5), # top left Vector2f(1, 1) # width, height ) +# output +Shape(native_handle = 0) ``` ![](../assets/shape_rectangle.png) @@ -298,12 +318,14 @@ A [`Circle`](@ref) is constructed from a center point and radius. We also need t As the number of outer vertices increases, the shape approaches a mathematical circle, but will also require more processing power. -```julia +```jldoctest; output = false circle = Circle( Vector2f(0, 0), # center 0.5, # radius 32 # n outer vertices ) +# output +Shape(native_handle = 0) ``` ![](../assets/shape_circle.png) @@ -312,13 +334,15 @@ circle = Circle( An [`Ellipse`](@ref) is a more generalized form of a `Circle`. It has two radii, the x- and y-radius: -```julia +```jldoctest; output = false ellipse = Ellipse( Vector2f(0, 0), # center 0.6, # x-radius 0.4, # y-radius 32 # n outer vertices ) +# output +Shape(native_handle = 0) ``` ![](../assets/shape_ellipse.png) @@ -327,7 +351,7 @@ ellipse = Ellipse( The most general form of convex shapes, [`Polygon`](@ref) is constructed using a vector of vertices, which will be sorted clockwise, then their [outer hull](https://en.wikipedia.org/wiki/Convex_hull) will be calculated, which results in the final convex polygon: -```julia +```jldoctest; output = false polygon = Polygon([ Vector2f(0.0, 0.75), Vector2f(0.75, 0.25), @@ -335,6 +359,8 @@ polygon = Polygon([ Vector2f(-0.5, -0.5), Vector2f(-0.75, 0.0) ]) +# output +Shape(native_handle = 0) ``` ![](../assets/shape_polygon.png) @@ -345,13 +371,15 @@ We note that a 4-vertex polygon is a rectangle. Therefore, if we want to render A [`RectangularFrame`](@ref) takes a top-left vertex, a width, a height, and the x- and y-width, the latter of which is the thickness of the frame along the x- and y-axes: -```julia +```jldoctest; output = false rectangular_frame = RectangularFrame( Vector2f(-0.5, 0.5), # top-left Vector2f(1, 1), # width, height 0.15, # x-thickness 0.15, # y-thickness ) +# output +Shape(native_handle = 0) ``` ![](../assets/shape_rectangular_frame.png) @@ -361,13 +389,15 @@ Note how the top left and size govern the position and size of the outer perimet For the round equivalent of a rectangular frame, we have [`CircularRing`](@ref), which takes a center, the radius of the outer perimeter, as well as the ring's thickness. Like `Circle` and `Ellipse`, we have to specify the number of outer vertices, which decides the smoothness of the ring: -```julia +```jldoctest; output = false circular_ring = CircularRing( Vector2f(0, 0), # center 0.5, # radius of outer circle 0.15, # thickness 32 # n outer vertices ) +# output +Shape(native_handle = 0) ``` ![](../assets/shape_circular_ring.png) @@ -378,7 +408,7 @@ As before, the center and radius determine the position and size of the outer pe A generalization of `CircularRing`, [`EllipticalRing`](@ref) has an ellipse as its outer shape. Its thickness along the horizontal and vertical dimensions are supplied separately, making it more flexible than `CircularRing`. -```julia +```jldoctest; output = false elliptcal_ring = EllipticalRing( Vector2f(0, 0), # center 0.6, # x-radius @@ -387,6 +417,8 @@ elliptcal_ring = EllipticalRing( 0.15, # y-thickness 32 # n outer vertices ) +# output +Shape(native_handle = 0) ``` ![](../assets/shape_elliptical_ring.png) @@ -459,9 +491,10 @@ We can access the centroid using [`get_centroid`](@ref). To move a shape a certa We can rotate all of a `Shape`'s vertices around a point in GL coordinates by calling [`rotate!`](@ref), which takes an `Angle` as its first argument: -```julia +```jldoctest; output = false # rotate shape around its center rotate!(shape, degrees(90), get_centroid(shape)) +# output ``` #### Color @@ -556,7 +589,7 @@ Wrap mode governs how the texture behaves when a vertice's texture coordinate co ![](../assets/texture_wrap_modes.png) !!! details "How to generate this Image" - ```julia + ```jldoctest; output = false using Mousetrap # compound widget that displays a texture with a label @@ -607,9 +640,9 @@ Wrap mode governs how the texture behaves when a vertice's texture coordinate co render_area = RenderArea() - image = Image() - create_from_file!(image, "docs/src/assets/logo.png") - # this assumes the script is run in `Mousetrap.jl` root + image = Image(1, 1) + # create_from_file!(image, "docs/src/assets/logo.png") + # uncomment above, this assumes the script is run in `Mousetrap.jl` root # replace RGBA(0, 0, 0, 0) pixels with rainbow color size = get_size(image) @@ -635,6 +668,8 @@ Wrap mode governs how the texture behaves when a vertice's texture coordinate co set_child!(window, box) present!(window) end + # output + 0 ``` Where the default wrap mode is `TEXTURE_WRAP_MODE_REPEAT`. @@ -687,7 +722,7 @@ Where `width` and `height` are the new sizes of the `RenderArea` widget, in pixe Using this information and some simple geometry, we can change the x- and y-radius dynamically whenever the `RenderArea` changes aspect ratio: -```julia +```jldoctest; output = false # define resize callback function on_resize(::RenderArea, width::Integer, height::Integer, shape::Shape) @@ -716,6 +751,8 @@ main() do app::Application set_child!(window, render_area) present!(window) end +# output +0 ``` ![](../assets/render_area_destretched.png) @@ -758,7 +795,7 @@ It's difficult to convey the result of MSAA using just pictures on a web page du ![](../assets/msaa_comparison.png) !!! details "How to generate this Image" - ```julia + ```jldoctest; output = false main() do app::Application window = Window(app) @@ -813,6 +850,8 @@ It's difficult to convey the result of MSAA using just pictures on a web page du set_child!(window, paned) present!(window) end + # output + 0 ``` --- @@ -849,7 +888,7 @@ Internally, a `GLTransform` is a 4x4 matrix of 32-bit floats. It is of size 4x4 At any time, we can directly access the underlying matrix of a `GLtransform` using `getindex` or `setindex!`: -```julia +```@repl transform = GLTransform() for i in 1:4 for j in 1:4 @@ -858,12 +897,6 @@ for i in 1:4 print("\n") end ``` -``` -1 0 0 0 -0 1 0 0 -0 0 1 0 -0 0 0 1 -``` We see that after construction, `GLTransform` is initialized as the identity transform. No matter the current state of the transform, we can reset it back to this identity matrix by calling [`reset!`](@ref). @@ -879,14 +912,16 @@ We can combine two transforms using [`combine_with`](@ref). If we wish to apply While we could apply the transform to each vertex of a `Shape` manually, then render the shape, it is much more performant to do this kind of math GPU-side. By registering the transform with a `RenderTask`, the transform will be forwarded to the vertex shaders, which, for the default vertex shader, is then applied to the shape's vertices automatically: -```julia +```jldoctest; output = false shape = Shape() -transform = Transform() +transform = GLTransform() translate!(transform, Vector2f(-0.5, 0.1)) rotate!(transform, degrees(180)) task = RenderTask(shape; transform = transform) +# output +RenderTask() ``` Where we used the `transform` keyword argument to specify the transform while leaving the other render task component unspecified. This means the transform is applied automatically during rendering, allowing us to take advantage of the increased performance gained from the GPU architecture. @@ -956,7 +991,7 @@ add_render_task!(render_area, task) ![](../assets/shader_hello_world.png) !!! details "How to generate this Image" - ```julia + ```jldoctest; output = false using Mousetrap main() do app::Application @@ -991,6 +1026,8 @@ add_render_task!(render_area, task) set_child!(window, frame) present!(window) end + # output + 0 ``` If we do not initialize the vertex- or fragment shader, the **default shader component will be used**. It may be instructive to see how the default shaders are defined, as any user-defined shader should build upon them. @@ -1100,7 +1137,7 @@ The following types can be assigned this way: We would therefore set the `_color_rgba` uniform value like so: -```julia +```jldoctest; output = false # create shader shader = Shader() create_from_string!(shader, SHADER_TYPE_FRAGMENT, """ @@ -1126,12 +1163,13 @@ task = RenderTask(shape; shader = shader) # shader bound to `shader` keyword arg # set uniform set_uniform_rgba!(task, "_color_rgba", RGBA(1, 0, 1, 1)) +# output ``` ![](../assets/shader_rgba_uniform.png) !!! details "How to generate this Image" - ```julia + ```jldoctest; output = false using Mousetrap main() do app::Application @@ -1169,6 +1207,8 @@ set_uniform_rgba!(task, "_color_rgba", RGBA(1, 0, 1, 1)) set_child!(window, frame) present!(window) end + # output + 0 ``` Where the name used in `set_uniform_*!` has to exactly match the variable name in GLSL. From b32cfe4e80f07be293db02bb237e30d8dbb368cd Mon Sep 17 00:00:00 2001 From: Clem Cords Date: Sun, 11 Feb 2024 19:25:37 +0100 Subject: [PATCH 21/31] added doctests --- docs/src/01_manual/10_theme_customization.md | 43 +++++++++++++------- 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/docs/src/01_manual/10_theme_customization.md b/docs/src/01_manual/10_theme_customization.md index 06fccbc..bfb41ae 100644 --- a/docs/src/01_manual/10_theme_customization.md +++ b/docs/src/01_manual/10_theme_customization.md @@ -46,7 +46,7 @@ At any point after the back-end has been initialized, we can swap the global the For example, to create a window that has a button to switch between light and dark themes in its header bar, we could do the following: -```julia +```jldoctest; output = false main() do app::Application window = Window(app) @@ -77,6 +77,8 @@ main() do app::Application push_front!(header_bar, swap_button) present!(window) end +# output +0 ``` --- @@ -110,12 +112,13 @@ Where `value` is the animations output value. By default, this will be in `[0, 1 Since a widget's opacity is already in `[0, 1]`, we can use the animations value directly: -```julia +```jldoctest; output = false to_animate = Button(Label("Fade Out")) animation = Animation(to_animate, seconds(1)) -on_tick!(animation, button) do self::Animation, value::Float64, target::Button +on_tick!(animation, to_animate) do self::Animation, value::Float64, target::Button set_opacity!(target, 1 - value) end +# output ``` Where we used `1 - value` to invert the range, such that the widget starts fully opaque and decreases in opacity. @@ -125,7 +128,7 @@ We can then start the animation using `play!`, for example, by clicking the butt ![](../assets/animation_fade_out.webm) !!! details "How to generate this Image" - ```julia + ```jldoctest; output = false using Mousetrap main() do app::Application window = Window(app) @@ -150,6 +153,8 @@ We can then start the animation using `play!`, for example, by clicking the butt set_child!(window, aspect_frame) present!(window); end + # output + 0 ``` For cyclical animations, we can use [`set_repeat_count!`](@ref) to specify the number of times the animation should loop, or `0` to loop infinitely. We can easily reverse an animation by setting [`set_is_reversed!`](@ref) to `true`. @@ -183,7 +188,7 @@ These functions are called on the `TransformBin` instance directly, we do not us For example, to make a button spin one time when it is clicked, we can use `TransformBin` and `Animation` as follows: -```julia +```jldoctest; output = false # animation target to_animate = Button(Label("Spin")) @@ -203,6 +208,7 @@ end connect_signal_clicked!(to_animate, animation) do self::Button, animation::Animation play!(animation) end +# output ``` ![](../assets/animation_spin.webm) @@ -224,13 +230,14 @@ Mousetrap uses [Cascading Style Sheets (CSS)](https://developer.mozilla.org/en-U We can define a CSS modifier class as a string, then compile that string using [`add_css!`](@ref), which makes that modifier class globally available: -```julia +```jldoctest; output = false # define modifier class `sharp-corners` add_css!(""" .sharp-corners { border-radius: 0%; } """) +# output ``` We can then apply this class to any widget using [`add_css_class!`](@ref), at which point the widgets' appearance will change accordingly. To remove the modifier, we call [`remove_css_class!`](@ref). A widget can have more than one modifier class. To list all applied CSS classes, we use [`get_css_classes`](@ref). @@ -239,7 +246,7 @@ For a list of CSS properties supported by Mousetrap, see [here](https://docs.gtk For example, the following defines a `ToggleButton` that, when toggled, applies the following CSS class to both the `Window` (which is a Widget), and its `HeaderBar`: -```julia +```jldoctest; output = false using Mousetrap add_css!(""" .custom { @@ -255,7 +262,7 @@ main() do app::Application set_title!(window, "Mousetrap.jl") button = ToggleButton() - connect_signal_toggled!(button, window) do self::ToggleButton, window::Window + connect_signal_toggled!(button, window) do self::ToggleButton, window::Widget if get_is_active(self) add_css_class!(window, "custom") add_css_class!(get_header_bar(window), "custom") @@ -267,6 +274,8 @@ main() do app::Application set_child!(window, button) present!(window) end +# output +0 ``` ![](../assets/css_style_pink_window.png) @@ -289,7 +298,7 @@ We can make an `Entry` or `TextView` use monospaced text by calling `add_css_cla The following implements `set_accent_color!`, which is not part of Mousetrap. `set_accent_color!` takes a widget, one of the below constants, as well as a boolean indicating whether the window should be opaque, as its arguments. When applied to a widget, this function changes that widgets color to one of the 5 pre-defined UI colors, such that their look fits well with the default UI theme: -```julia +```jldoctest accent_color; output = false using Mousetrap # define widget colors @@ -324,13 +333,16 @@ function set_accent_color!(widget::Widget, color, opaque = true) add_css_class!(widget, "opaque") end end +# output +set_accent_color! (generic function with 2 methods) ``` + Users are encouraged to just copy the above code into their own project, for `set_accent_color!` to become available. We can use this function like so: -```julia +```jldoctest accent_color; output = false # widget factory create_widget() = Button(Label("TEST")) @@ -338,9 +350,9 @@ create_widget() = Button(Label("TEST")) column_view = ColumnView() # column 1: whether `opaque` was set to true -column = push_back_column!(column_view, " ") -set_widget_at!(column_view, column, 1, Label("!opaque")) -set_widget_at!(column_view, column, 2, Label("opaque")) +first_column = push_back_column!(column_view, " ") +set_widget_at!(column_view, first_column, 1, Label("!opaque")) +set_widget_at!(column_view, first_column, 2, Label("opaque")) for color in [ WIDGET_COLOR_DEFAULT, # column 2: default look of a widget @@ -360,6 +372,7 @@ for color in [ set_accent_color!(widget, color, true) set_widget_at!(column_view, column, 2, widget) end +# output ``` Here, we created a column view that shows all permutations of the arguments of `set_accent_color!`. We defined `create_widget() = Button(Label("TEST"))`, therefore each cell will have a button with a label: @@ -392,7 +405,7 @@ For names of palette colors other than `accent_bg_color`, see [here](https://git `Animation` offers an in-engine way to do animations, which is usually preferred. However, [CSS animations](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_animations/Using_CSS_animations) are supported as well, and are defined as they would be in pure CSS: -```julia +```jldoctest; output = false # define CSS animation and modifier add_css!(""" @keyframes spin-animation { @@ -419,6 +432,8 @@ main() do app::Application set_child!(window, AspectFrame(1.0, button)) present!(window) end +# output +0 ``` ![](../assets/css_style_animation_spin.webm) From 06362da1e575579b38676ac3f523d47503a11c3f Mon Sep 17 00:00:00 2001 From: Clem Cords Date: Sun, 11 Feb 2024 19:26:13 +0100 Subject: [PATCH 22/31] restored 08_menus --- docs/src/01_manual/08_menus.md | 343 +++++++++++++++++++++++++++++++++ 1 file changed, 343 insertions(+) create mode 100644 docs/src/01_manual/08_menus.md diff --git a/docs/src/01_manual/08_menus.md b/docs/src/01_manual/08_menus.md new file mode 100644 index 0000000..ed1c885 --- /dev/null +++ b/docs/src/01_manual/08_menus.md @@ -0,0 +1,343 @@ +```@meta +DocTestSetup = quote + using Mousetrap + function Window(app::Application) + out = Mousetrap.Window(app) + set_tick_callback!(out, out) do clock, self + close!(self) + return TICK_CALLBACK_RESULT_DISCONTINUE + end + return out + end + + app = Application("test.app") + action = Action("test.action", app) + submenu_content() = Separator +end +``` + +# Chapter 8: Menus + +In this chapter, we will learn: ++ How to create complex, nested menus ++ How to display menus using `PopoverMenu` and `MenuBar` ++ Best-practice style guides for menus + +--- + +In the [chapter on actions](./03_actions.md) we learned that we can trigger an action using [`Button`](@ref), by assigning an action to it using [`set_action!`](@ref). +This works if we want to have a GUI element that has one or maybe a few actions. In practice, an application can have hundreds of different actions. Asking users to trigger these using an equal number of buttons would be unsustainable. For situations like these, we should instead turn to **menus**. + +## MenuModel Architecture + +In Mousetrap, menus follow the [model-view architectural pattern](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller). To create a menu, we need +the menu model, which holds information about how the menu is structured, along with a view, which takes the models and transforms them into an interactable +widget users can manipulate. Changing the model changes the view. + +!!! details "Running Snippets from this Section" + + To follow along with code snippets from this section, we can use the following `main.jl` file: + ```jldoctest; output = false + using Mousetrap + main() do app::Application + + window = Window(app) + + # create dummy action + action = Action("dummy.action", app) do x + println("triggered.") + end + + # model that we will be modifying in the snippet + root = MenuModel() + + # snippet goes here + + # display menu + add_submenu!(root, "Title", root) + view = PopoverButton(PopoverMenu(root)) + set_child!(window, view) + present!(window) + end + # output + 0 + ``` + +## Menu Items + +[`MenuModel`](@ref), the model component of Mousetrap menus, is a list of **menu items** in a specific order. If item `A` is added before item `B` at runtime, then `A` will appear above item `B`. There are multiple different types of menu items, all with their own purpose. The function we choose to add an item to the model determines the item's type. There are four types of menu items, which we will go over in this section. + +### Item Type #1: Action + +The first and most simple item type is called "action". Added via [`add_action!`](@ref), which takes both a label and an [`Action`](@ref) instance, this item is a simple button with text. If the user clicks the button, the action is executed. + +Similar to `Button`, if the action is disabled (via [`set_enabled!`](@ref)) or does not yet have a callback registered, the menu item will appear "greyed out" and cannot be activated. + +```jldoctest menu_model; output = false +model = MenuModel() +add_action!(model, "Action Item #1", action) +add_action!(model, "Action item #2", action) +add_action!(model, "Action item #3", action) +# output +``` + +![](../assets/menu_model_actions.png) + +### Item Type #2: Widgets + +Secondly, we have perhaps the most powerful type of item: A custom widget. We add an item of this type using [`add_widget!`](@ref), which only takes a single argument, the widget itself. This widget can be arbitrarily complex, though it is usually not advisable to insert an entire `ColumnView` into a tiny menu. Good-practice examples include `Button`, `Entry`, `CheckButton`, `ToggleButton`, `Switch`, and `SpinButton`, all of which are intractable. + +We do not supply an `Action` instance with this item, if we want the user interacting with the menu item to trigger behavior, we will have to connect that behavior to the signals of the widget we inserted into the menu, or any of its event controllers. + +```jldoctest menu_model; output = false +add_widget!(model, hbox(Label("Choose Value: "), SpinButton(0, 1, 0.01))) +add_widget!(model, Scale(0, 1, 0.01, ORIENTATION_HORIZONTAL)) +add_widget!(model, hbox(Label("Enter Text: "), Entry())) +# output +``` + +![](../assets/menu_model_widgets.png) + +Widgets are the most flexible type of menu item. They should be used with caution, and only if absolutely necessary. It is often better to create an action that opens a separate [`Window`](@ref) that contains the widget, as opposed to directly adding the widget to the menu. + +### Item Type #3: Submenu + +`MenuModel` can be **nested**, which means we can insert a `MenuModel` into another `MenuModel`. This is similar to `ListView` or file-system tree: a folder can contain regular files (menu items), or it can contain other folders (menu models), which in turn can also contain another file or folder, etc. `MenuModel` can similarly be infinitely nested, though it is usually not recommended to go deeper than two or three levels. + +We call a `MenuModel` that is nested within another model a **submenu**. It will show up as a button with a label, along with an indicator that it is a submenu, usually a `>` shape. Clicking this item will reveal the submenu. + +To add this type of submenu item, we use [`add_submenu!`](@ref), which takes a title and another menu model: + +```jldoctest menu_model; output = false +submenu_01 = MenuModel() +add_widget!(submenu_01, submenu_content()) +add_submenu!(model, "Submenu #1", submenu_01) + +submenu_02 = MenuModel() +add_widget!(submenu_02, submenu_content()) +add_submenu!(model, "Submenu #2", submenu_02) +# output +``` +![](../assets/menu_model_submenu_outer.png) + +Clicking on of these items will reveal the submenu content: + +![](../assets/menu_model_submenu_inner.png) + +Where `submenu_content()` returns a simple place-holder widget, in reality, this will be many other menu items or other submenus. + +### Item Type #4: Icons + +A prettier analog to the "action"-type menu item, an "icon"-type item. Added via [`add_icon!`](@ref), it takes an [`Icon`](@ref), along with an action. The entire menu item will be just the icon, it may not have any text. If the icon is clicked, the action is executed. + +```julia +add_icon!(model, Icon(#=...=#), action) +add_icon!(model, Icon(#=...=#), action) +add_icon!(model, Icon(#=...=#), action) +add_icon!(model, Icon(#=...=#), action) +``` +![](../assets/menu_model_icons.png) + +Where we used the default Gnome icons for weather indicators as placeholders. + +When creating a menu item with an action, we have to decide whether to use a simple text label or an icon. We may not have both. + +### Item Type #5: Sections + +Lastly, we have **sections**. Sections are like submenus, in that they are a menu inside another menu. The difference is in the way this inner menu is displayed. + +When adding a submenu with `add_submenu!`, a single new item will appear in the outer menu. When clicked, the menu "slides" to reveal the submenu. + +With sections, the inner menu is instead inserted directly into the outer menu; both are shown at the same time. + +We add a "section"-type item using [`add_section!`](@ref), which takes another menu model and a title, which will be used as a header for the section: + +```julia +section = MenuModel() +add_action!(section, "Section Item #01", #= action =#) +add_action!(section, "Section Item #02", #= action =#) +add_action!(section, "Section Item #03", #= action =#) +add_section!(model, "Section Label", section) +``` +![](../assets/menu_model_section.png) + + +We see that the section label, `"Section Label"` in this case, is displayed above all its items, which are inserted into the outer menu. In this way, sections can be helpful to group multiple menu items, which makes a menu easier to parse without adding another nested level via a submenu. + +#### Section Formats + +[`add_section!`](@ref) takes one additional, optional argument, which is a [`SectionFormat`](@ref). This enum has a number of values that govern how the section is displayed: + ++ `SECTION_FORMAT_CIRCULAR_BUTTONS` displays all its elements in circular buttons ++ `SECTION_FORMAT_HORIZONTAL_BUTTONS`: display its elements as a row of rectangular buttons ++ `SECTION_FORMAT_INLINE_BUTTONS`: display all buttons right of the section heading + +The following shows these section formats: + +```jldoctest; output = false +icon_model = MenuModel() + +# generate a menu model with the 4 weather icons, then add as section with given format +function add_icon_section(title::String, format::SectionFormat) + section = MenuModel() + add_icon!(section, Icon(#=...=#), action) + add_icon!(section, Icon(#=...=#), action) + add_icon!(section, Icon(#=...=#), action) + add_icon!(section, Icon(#=...=#), action) + add_section!(icon_model, title, section, format) +end + +add_icon_section("Normal", SECTION_FORMAT_NORMAL) +add_icon_section("Horizontal Buttons", SECTION_FORMAT_HORIZONTAL_BUTTONS) +add_icon_section("Inline Buttons: ", SECTION_FORMAT_INLINE_BUTTONS) +add_icon_section("Circular Buttons", SECTION_FORMAT_CIRCULAR_BUTTONS) +# output +``` + +![](../assets/menu_model_section_formats.png) + +Using `SectionFormat` and mixing several menu item types, we can construct arbitrarily complex menus. We should realize that the highest priority when constructing menu items is the **user experience**. Presenting users with a giant, deeply nested mess of buttons may be very functional, but it may not be very usable to anyone but the developers themself. + +--- + +## Displaying Menus + +Now that we have learned to construct the menu **model**, we should turn our attention to the **view**, a widget displaying a `MenuModel`. + +### PopoverMenu + +[`PopoverMenu`](@ref) is a sibling of [`Popover`](@ref). It consists of a small dialog that is only displayed when we ask it to. While `Popover` displays an arbitrary widget, `PopoverMenu` displays a menu model. + +For ease of use, it's easiest to connect the `PopoverMenu` to a [`PopoverButton`](@ref), just like we did with `Popover`: + +```julia +model = MenuModel() + +# fill `model` here + +popover_menu = PopoverMenu(model) +popover_button = PopoverButton(popover_menu) + +# add the button to the window so we can click it +set_child!(window, popover_button) +``` + +The `PopoverMenu`-`PopoverButton` combo should be reserved for **context menus**, which are menus that act on some local part of the application. For a menu that affects the entire application and should be accessible at all times, we should use the next menu model view instead. + +### MenuBar + +Familiar to any user of a modern desktop GUI, [`MenuBar`](@ref) is a widget that is usually displayed at the top of the main application window: + +![](../assets/menu_bar.png) + +We see that `MenuBar` is a horizontal list of items. When the user clicks on one of the items, a nested menu opens. Just like before, menus can be nested a theoretically infinite number of times. + +Unlike `PopoverMenu`, `MenuBar` requires its `MenuModel` to have a certain structure: **all top-level items have to be submenus**. + +What does this mean? Let's work through the menu shown in the image above. We created it using the following snippet: + +```julia +root = MenuModel() + +file_submenu = MenuModel() +add_action!(file_submenu, "Open", #= action =#) + +file_recent_submenu = MenuModel() +add_action!(file_recent_submenu, "Project 01", #= action =#) +add_action!(file_recent_submenu, "Project 02", #= action =#) +add_action!(file_recent_submenu, "Other...", #= action =#) +add_submenu!(file_submenu, "Recent...", file_recent_submenu) + +add_action!(file_submenu, "Save", #= action =#) +add_action!(file_submenu, "Save As...", #= action =#) +add_action!(file_submenu, "Exit", #= action =#) + +help_submenu = MenuModel() +add_submenu!(root, "File", file_submenu) +add_submenu!(root, "Help", help_submenu) + +menubar = MenuBar(model) +``` + +Where in a real application, each item will have a different action. + +This code can be quite hard to read. To make the nesting easier to understand, we'll write it out as if it were a file system folder structure: + +``` +model \ + File \ + Open + Recent... \ + Project 01 + Project 02 + Other... + Save + Save As + Exit + Help \ + (...) +``` + +Where any line suffixed with a `\` is a submenu. + +We see that we have four models in total. The top-most menu model is called `root`, it is what `MenuBar` will be initialized with. +Next, we have the model called `file_submenu`, which has the title `File`. It contains five items, titled `Open`, `Recent...`, `Save`, `Save As`, and `Exit`. `Recent...` is a submenu-type item, created from a `MenuModel` called `file_recent_submenu` in the above code. This model, in turn, has three items. On the same level as `File`, we have a second menu `Help`. + +The **top-level** menu is `root`. It is used as the argument for the constructor of `MenuBar`. We see that all direct children of `root` (`File` and `Help`) **are themselves submenus** (they were added using `add_submenu!`). + +No direct child of `root` is an "action"-, "widget"-, "icon"- or "section"-type item. This is what is required for `MenuBar`. All top-level items have to be submenus. + +!!! warning + Due to a bug in the backend, as of `v0.3.0`, a menu model used for a `MenuBar` **may not have a "widget"-type item in a submenu of a submenu**. + + This means we *can* add a widget to any submenu of `root`, but we may not add + a widget to any submenu that is nested any deeper than a direct child of `root`. + + This bug does not affect `PopoverMenu`, for whom we can put a widget at any + depth. `PopoverMenu` has no requirement as to the structure of its menu model, while `MenuBar` requires that all top-level items are submenus and that no submenu of a submenu may have a "widget"-type item. + +## Main Menu (macOS only) + +!!! compat + Features from this section are only available with Mousetrap `v0.3.1` or newer, and should only be used by applications targeting macOS. We can use `Sys.isapple` to verify the user's operating system. + +On macOS, applications are able to target the [**main menu**](https://support.apple.com/en-gb/guide/mac-help/mchlp1446/mac), which is the menubar at the very top of the screen (outside the Mousetrap window). This bar contains the Apple menu, as well as a native menubar with application-specific options. To bind a `Mousetrap.MenuModel` to this menubar, we use `set_menubar` on our `Application` instance: + +```julia +main() do app::Application + model = MenuModel() + # fill model here + + if Sys.isapple() + set_menubar(app, model) # associate model with the Apple main menu + end +end +``` + +Note that it may be necessary to call `set_menubar` again when the `MenuModel` is modified in order for the main menu to be updated. + +--- + +## Style End Note + +Menus are extremely powerful and complex to construct. With practice and good software / UI design, we can create deep, complex menus that are still easy to understand and use. We, as developers, should make this our priority. + +Some additional notes: + +### Ellipses + +Some may be curious as to why some menu items have `...` added at the end of their labels, while others do not. This is not a universal standard, but it is common for `...` to indicate that clicking this item will open another window, submenu, or dialog. If clicking an item simply triggers an action (such as `Save` or `Exit`), `...` is omitted. If the item opens a window, widget, or submenu, `...` is appended to the menu label, as is the case with `Recent...` above. + +### Maximum Menu Depth + +Regarding menu depth, the best practice is to never go deeper than three levels. The above example with `File > Recent... > Project 01` shows a good situation in which a 3-level-deep menu may be justified. Going any deeper is rarely a good course of action. Adding a section should always be considered *before* deciding to add a submenu. + +### Section Grouping + +Lastly, some schools of UI design believe that **every menu item should be inside a section**. For example, if we were to follow this philosophy for our above `MenuBar` example, we would redesign it like this: + +![](../assets/menu_bar_with_sections.png) + +This adds considerable complexity to the code (adding 4 models, one for each section, making our total 8). In return, items are grouped logically, and each item gets a "heading", which helps make long menus easier to understand. For this small example, this is most likely unnecessary, but it will be more attractive for a menubar with dozens of items. + +In the end, each UI designer should decide for themselves which technique to use. What all should agree on, however, is that ease of use for the end-user is the most important thing. It should trump ease of development in every case. If something is harder for us developers but makes it easier for our users, we should go through the effort of doing it. From 65dfb2f6ca5b9529adf3655afb84635319ce9e80 Mon Sep 17 00:00:00 2001 From: Clem Cords Date: Thu, 15 Feb 2024 17:36:48 +0100 Subject: [PATCH 23/31] remove temp test --- test/runtests.jl | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/test/runtests.jl b/test/runtests.jl index bba8dd0..5318ec3 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -2332,10 +2332,7 @@ function test_viewport(::Container) end end -function test_window(::Container) - @testset "Window" begin - - + main() do app::Application window = Window(app) @@ -2852,6 +2849,5 @@ main(Main.app_id) do app::Application close!(window) #quit!(app) - println("TODO: done.") end From b1ad216706a3794feca3d47e26ceffb734965f67 Mon Sep 17 00:00:00 2001 From: Clem Cords Date: Thu, 15 Feb 2024 17:46:26 +0100 Subject: [PATCH 24/31] disable CI until mac issues is fixed --- .github/{workflows/CI.yml => ~CI.yml} | 0 docs/{src/01_manual => _temp}/01_installation.md | 0 docs/{src/01_manual => _temp}/02_signals.md | 0 docs/{src/01_manual => _temp}/03_actions.md | 0 docs/{src/01_manual => _temp}/04_widgets.md | 0 docs/{src/01_manual => _temp}/05_event_handling.md | 0 docs/src/01_manual/06_image.md | 4 ++++ 7 files changed, 4 insertions(+) rename .github/{workflows/CI.yml => ~CI.yml} (100%) rename docs/{src/01_manual => _temp}/01_installation.md (100%) rename docs/{src/01_manual => _temp}/02_signals.md (100%) rename docs/{src/01_manual => _temp}/03_actions.md (100%) rename docs/{src/01_manual => _temp}/04_widgets.md (100%) rename docs/{src/01_manual => _temp}/05_event_handling.md (100%) diff --git a/.github/workflows/CI.yml b/.github/~CI.yml similarity index 100% rename from .github/workflows/CI.yml rename to .github/~CI.yml diff --git a/docs/src/01_manual/01_installation.md b/docs/_temp/01_installation.md similarity index 100% rename from docs/src/01_manual/01_installation.md rename to docs/_temp/01_installation.md diff --git a/docs/src/01_manual/02_signals.md b/docs/_temp/02_signals.md similarity index 100% rename from docs/src/01_manual/02_signals.md rename to docs/_temp/02_signals.md diff --git a/docs/src/01_manual/03_actions.md b/docs/_temp/03_actions.md similarity index 100% rename from docs/src/01_manual/03_actions.md rename to docs/_temp/03_actions.md diff --git a/docs/src/01_manual/04_widgets.md b/docs/_temp/04_widgets.md similarity index 100% rename from docs/src/01_manual/04_widgets.md rename to docs/_temp/04_widgets.md diff --git a/docs/src/01_manual/05_event_handling.md b/docs/_temp/05_event_handling.md similarity index 100% rename from docs/src/01_manual/05_event_handling.md rename to docs/_temp/05_event_handling.md diff --git a/docs/src/01_manual/06_image.md b/docs/src/01_manual/06_image.md index db97d4e..d6f6269 100644 --- a/docs/src/01_manual/06_image.md +++ b/docs/src/01_manual/06_image.md @@ -9,6 +9,10 @@ DocTestSetup = quote end return out end + + function present!(chooser::ColorChooser) + # noop + end end ``` From 4ea70c4e36196afe89bcd6b3f9eb9df5387f95b0 Mon Sep 17 00:00:00 2001 From: Clem Cords Date: Thu, 15 Feb 2024 17:57:34 +0100 Subject: [PATCH 25/31] doc tests passing --- Project.toml | 2 +- docs/{_temp => src/01_manual}/01_installation.md | 0 docs/{_temp => src/01_manual}/02_signals.md | 0 docs/{_temp => src/01_manual}/03_actions.md | 0 docs/{_temp => src/01_manual}/04_widgets.md | 0 docs/{_temp => src/01_manual}/05_event_handling.md | 0 docs/src/01_manual/06_image.md | 7 +------ docs/src/01_manual/08_menus.md | 6 +++--- 8 files changed, 5 insertions(+), 10 deletions(-) rename docs/{_temp => src/01_manual}/01_installation.md (100%) rename docs/{_temp => src/01_manual}/02_signals.md (100%) rename docs/{_temp => src/01_manual}/03_actions.md (100%) rename docs/{_temp => src/01_manual}/04_widgets.md (100%) rename docs/{_temp => src/01_manual}/05_event_handling.md (100%) diff --git a/Project.toml b/Project.toml index 2c3907a..f67a550 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "Mousetrap" uuid = "5deeb4b9-6e04-4da7-8b7f-c77fb1eae65e" authors = ["C.Cords "] -version = "0.3.1" +version = "0.3.4" [deps] BinaryBuilder = "12aac903-9f7c-5d81-afc2-d9565ea332ae" diff --git a/docs/_temp/01_installation.md b/docs/src/01_manual/01_installation.md similarity index 100% rename from docs/_temp/01_installation.md rename to docs/src/01_manual/01_installation.md diff --git a/docs/_temp/02_signals.md b/docs/src/01_manual/02_signals.md similarity index 100% rename from docs/_temp/02_signals.md rename to docs/src/01_manual/02_signals.md diff --git a/docs/_temp/03_actions.md b/docs/src/01_manual/03_actions.md similarity index 100% rename from docs/_temp/03_actions.md rename to docs/src/01_manual/03_actions.md diff --git a/docs/_temp/04_widgets.md b/docs/src/01_manual/04_widgets.md similarity index 100% rename from docs/_temp/04_widgets.md rename to docs/src/01_manual/04_widgets.md diff --git a/docs/_temp/05_event_handling.md b/docs/src/01_manual/05_event_handling.md similarity index 100% rename from docs/_temp/05_event_handling.md rename to docs/src/01_manual/05_event_handling.md diff --git a/docs/src/01_manual/06_image.md b/docs/src/01_manual/06_image.md index d6f6269..7b040cf 100644 --- a/docs/src/01_manual/06_image.md +++ b/docs/src/01_manual/06_image.md @@ -9,10 +9,6 @@ DocTestSetup = quote end return out end - - function present!(chooser::ColorChooser) - # noop - end end ``` @@ -121,7 +117,7 @@ The function called when the dialog is dismissed is registered using `on_cancel! We would use these two functions like so: -```jldoctest; output = false +```julia color_chooser = ColorChooser("Choose Color") # react to user selection @@ -135,7 +131,6 @@ on_cancel!(color_chooser) do self::ColorChooser end present!(color_chooser) -# output ``` At any point, we can also access the last selected color by calling [`get_color`](@ref) on the `ColorChooser` instance. diff --git a/docs/src/01_manual/08_menus.md b/docs/src/01_manual/08_menus.md index ed1c885..808bb0d 100644 --- a/docs/src/01_manual/08_menus.md +++ b/docs/src/01_manual/08_menus.md @@ -12,7 +12,7 @@ DocTestSetup = quote app = Application("test.app") action = Action("test.action", app) - submenu_content() = Separator + submenu_content() = Separator() end ``` @@ -50,11 +50,11 @@ widget users can manipulate. Changing the model changes the view. # model that we will be modifying in the snippet root = MenuModel() - + model = MenuModel() # snippet goes here # display menu - add_submenu!(root, "Title", root) + add_submenu!(root, "Title", model) view = PopoverButton(PopoverMenu(root)) set_child!(window, view) present!(window) From ded10c522fc1e17dc78e615d8b8737e884d3236b Mon Sep 17 00:00:00 2001 From: Clem Cords Date: Thu, 15 Feb 2024 18:14:16 +0100 Subject: [PATCH 26/31] added warning about render area being disabled --- docs/src/01_manual/09_native_rendering.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/docs/src/01_manual/09_native_rendering.md b/docs/src/01_manual/09_native_rendering.md index e55933f..58baa06 100644 --- a/docs/src/01_manual/09_native_rendering.md +++ b/docs/src/01_manual/09_native_rendering.md @@ -61,9 +61,20 @@ In this chapter we will learn: To make this change permanent, we can paste the above line into the `~/.bashrc` text file, which will be loaded automatically anytime a terminal starts. -!!! details "Manually Disabling the OpenGL Component" +!!! warning "Manually Disabling the OpenGL Component" We can disable all features from this chapter by setting the environment variable `MOUSETRAP_DISABLE_OPENGL_COMPONENT` to `TRUE`. This may be necessary for machines that do not have an OpenGL 3.3-compatible graphics card driver. See [here](https://github.com/Clemapfel/Mousetrap.jl/issues/25#issuecomment-1731349366) for more information. +!!! danger "RenderArea: OpenGL component is disabled" + Newer macOS machines and some Linux machines with proprietary graphics drivers may encounter the following error when the initializing the `RenderArea` widget: + + ``` + In RenderArea(): trying to instantiate RenderArea, but the OpenGL component is disabled. + ``` + + Mousetrap uses a custom, shared OpenGL state that is necessary for `RenderArea` to function. On some machines, creation of this state may fail, at which point Mousetrap has no option other than disabling the `RenderArea` widget. We may still use all other features of Mousetrap, this only affects `RenderArea`, specifically. **This also means we are unable to use any featues described in this chapter**. + + While this issue is technically fixable, Mousetrap developers would need access to the specific hardware configurations that are causing the issues, which is not monetarily viable. + --- In the [chapter on widgets](04_widgets.md), we learned that we can create new widgets by combining already predefined widgets as a *compound widget*. We can create a new widget that has a `Scale`, but we cannot render our own scale with, for example, a square knob. In this chapter, this will change. From c7046874286f0979d808ec5e2d064785d248d9fa Mon Sep 17 00:00:00 2001 From: Clem Cords Date: Thu, 22 Feb 2024 19:38:19 +0100 Subject: [PATCH 27/31] up version --- Project.toml | 2 +- README.md | 2 ++ src/Mousetrap.jl | 4 ++-- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Project.toml b/Project.toml index f67a550..1b7efd3 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "Mousetrap" uuid = "5deeb4b9-6e04-4da7-8b7f-c77fb1eae65e" authors = ["C.Cords "] -version = "0.3.4" +version = "0.3.2" [deps] BinaryBuilder = "12aac903-9f7c-5d81-afc2-d9565ea332ae" diff --git a/README.md b/README.md index 294ad6e..baf2af0 100644 --- a/README.md +++ b/README.md @@ -122,6 +122,8 @@ display(screen, scatter(rand(123))) Since `v0.3.0`, Mousetrap is fully portable. All features are available for all 64-bit versions of Linux, FreeBSD, macOS, and Windows. +> **WARNING**: macOS systems with a newer architecture, such as the M1/M2 macs, seem to be causing an issue when using the `RenderArea` widget, forcing it to be disabled. Only this single widget is affected, see the [manual section on native rendering](http://clemens-cords.com/mousetrap/01_manual/09_native_rendering/) for more information. + > **Note**: Linux systems running Wayland may require additional configuration before the `RenderArea` widget becomes available. See [here](http://clemens-cords.com/mousetrap/01_manual/09_native_rendering/) for more information. > **Note**: Ubuntu systems using proprietary NVIDIA drivers may encounter a crash on initialization, a fix is available [here](https://github.com/Clemapfel/Mousetrap.jl/issues/25#issuecomment-1731349366). diff --git a/src/Mousetrap.jl b/src/Mousetrap.jl index 9dffbf2..96be059 100644 --- a/src/Mousetrap.jl +++ b/src/Mousetrap.jl @@ -12,11 +12,11 @@ GitHub: https://github.com/clemapfel/mousetrap.jl Documentation: http://clemens-cords.com/mousetrap/ -Copyright © 2023 C.Cords, Licensed under lGPL-3.0 +Copyright © 2024 C.Cords, Licensed under lGPL-3.0 """ module Mousetrap - const VERSION = v"0.3.1" + const VERSION = v"0.3.2" ####### detail.jl From a137f6ffc89d537a00ef9f83437f64ea7b391e64 Mon Sep 17 00:00:00 2001 From: Clem Cords Date: Thu, 22 Feb 2024 19:59:34 +0100 Subject: [PATCH 28/31] test passing --- test/runtests.jl | 135 +++++++++++++++++++++++++++++------------------ 1 file changed, 85 insertions(+), 50 deletions(-) diff --git a/test/runtests.jl b/test/runtests.jl index 5318ec3..34816a7 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -2332,46 +2332,6 @@ function test_viewport(::Container) end end - -main() do app::Application - window = Window(app) - - # create column view with columns - column_view = ColumnView() - - row_index_column = push_back_column!(column_view, " ") - count_column = push_back_column!(column_view, "#") - name_column = push_back_column!(column_view, "Name") - weight_column = push_back_column!(column_view, "Weight") - unit_column = push_back_column!(column_view, "Units") - - set_expand!.((row_index, count_column, name_column, weight_column, unit_column), true) - - # fill columns with text - for i in 1:100 - row = [ - Label(string(i)), # row index - Label(string(rand(0:99))), # count - Label(rand(["Apple", "Orange", "Banana", "Kumquat", "Durian", "Mangosteen"])), # name - Label(string(rand(0:100))), # weight - Label(string(rand(["mg", "g", "kg", "ton"]))) # unit - ] - - set_horizontal_alignment!.(row, ALIGNMENT_START) - push_back_row!(column_view, row...) - end - - # create viewport, this will add a scrollbar - scrolled_viewport = Viewport() - set_propagate_natural_width!(scrolled_viewport, true) # hides horizontal scrollbar - set_child!(scrolled_viewport, column_view) - - # show both in window - set_child!(window, scrolled_viewport) - present!(window) - -end - ### WIDGET function test_widget(widget::Container) @@ -2759,13 +2719,93 @@ function test_render_area(::Container) end end +function test_window(::Container) + @testset "Window" begin + + window = Window(Main.app[]) + other_window = Window(Main.app[]) + + Base.show(devnull, window) + @test Mousetrap.is_native_widget(window) + + close_request_called = Ref{Bool}(false) + connect_signal_close_request!(window, close_request_called) do self::Window, close_request_called + close_request_called[] = true + return WINDOW_CLOSE_REQUEST_RESULT_ALLOW_CLOSE + end + + activate_default_widget_called = Ref{Bool}(false) + connect_signal_activate_default_widget!(window, activate_default_widget_called) do self::Window, activate_default_widget_called + activate_default_widget_called[] = true + return nothing + end + + activate_focused_widget_called = Ref{Bool}(false) + connect_signal_activate_focused_widget!(window, activate_focused_widget_called) do self::Window, activate_focused_widget_called + activate_focused_widget_called[] = true + return nothing + end + + @test get_destroy_with_parent(window) == false + set_destroy_with_parent!(window, true) + @test get_destroy_with_parent(window) == true + + @test get_focus_visible(window) == true + set_focus_visible!(window, false) + @test get_focus_visible(window) == false + + @test get_has_close_button(window) == true + set_has_close_button!(window, false) + @test get_has_close_button(window) == false + + @test get_is_decorated(window) == true + set_is_decorated!(window, false) + @test get_is_decorated(window) == false + + @test get_is_modal(window) == false + set_is_modal!(window, true) + @test get_is_modal(window) == true + + set_title!(window, "test") + @test get_title(window) == "test" + + button = Entry() + set_child!(window, button) + set_default_widget!(window, button) + activate!(button) + + set_transient_for!(other_window, window) + + #@test activate_default_widget_called[] == true + #@test activate_focused_widget_called[] == true + + @test get_header_bar(window) isa HeaderBar + + @test get_hide_on_close(window) == false + set_hide_on_close!(window, true) + @test get_hide_on_close(window) == true + + @test get_is_closed(window) == true + present!(window) + @test get_is_closed(window) == false + set_minimized!(window, true) + set_maximized!(window, true) + + close!(other_window) + close!(window) + @test get_is_closed(window) == true + destroy!(window) + destroy!(other_window) + end +end + ### MAIN main(Main.app_id) do app::Application Main.app[] = app Main.window[] = Window(app) - set_is_decorated!(window, false) # prevent user from closing the window during tests + set_is_decorated!(Main.window[], false) # prevent user from closing the window during tests theme = IconTheme(Main.window[]) Main.icon[] = Icon() @@ -2773,11 +2813,9 @@ main(Main.app_id) do app::Application container = Container() viewport = Viewport() set_child!(viewport, container) - set_child!(window, viewport) + set_child!(Main.window[], viewport) connect_signal_realize!(container, window) do container::Container, window - - temp = """ test_action(container) test_adjustment(container) test_alert_dialog(container) @@ -2840,14 +2878,11 @@ main(Main.app_id) do app::Application test_viewport(container) test_widget(container) test_window(container) - """ return nothing end - present!(window) - close!(window) - #quit!(app) - - println("TODO: done.") + present!(Main.window[]) + close!(Main.window[]) + quit!(app) end From 6a1f8f52617f1f5b3b4329732120a15bed2a509a Mon Sep 17 00:00:00 2001 From: Clem Cords Date: Wed, 28 Feb 2024 14:46:53 +0100 Subject: [PATCH 29/31] updated version --- Project.toml | 4 ++-- jll/build_tarballs.jl | 7 +++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/Project.toml b/Project.toml index 1b7efd3..5e9e482 100644 --- a/Project.toml +++ b/Project.toml @@ -18,7 +18,7 @@ libcxxwrap_julia_jll = "3eaa8342-bff7-56a5-9981-c04077f7cee7" mousetrap_jll = "0e90efc8-2bbd-550f-bf3c-306a2edaaeef" [compat] -CxxWrap = "0.14.0" -mousetrap_jll = "0.3.0" +CxxWrap = "^0.14.0" +mousetrap_jll = "^0.3.0" GTK4_jll = "4.10" Glib_jll = "2.76" \ No newline at end of file diff --git a/jll/build_tarballs.jl b/jll/build_tarballs.jl index 591fff8..9b4664a 100644 --- a/jll/build_tarballs.jl +++ b/jll/build_tarballs.jl @@ -7,8 +7,8 @@ version = v"0.3.0" # Collection of sources required to complete build sources = [ - GitSource("https://github.com/Clemapfel/mousetrap.git", "ffa28d0bd569118320a6bb063286edfb35fc0429"), - GitSource("https://github.com/Clemapfel/mousetrap_julia_binding.git", "6b838ea238e118d694ffc1b3a1e0225441c8cfb3") + GitSource("https://github.com/Clemapfel/mousetrap.git", "aab1826318c43f0172d1ccd14faa06e7e61eba08"), + GitSource("https://github.com/Clemapfel/mousetrap_julia_binding.git", "ffba2656ef05ab4dc052e6d53ee524b6f5e2371b") ] # Bash recipe for building across all platforms @@ -34,8 +34,7 @@ platforms = filter(p -> nbits(p) == 64, supported_platforms()) # The products that we will ensure are always built products = [ LibraryProduct("libmousetrap", :mousetrap), - LibraryProduct("libmousetrap_julia_binding", :mousetrap_julia_binding), - FileProduct("") + LibraryProduct("libmousetrap_julia_binding", :mousetrap_julia_binding) ] x11_platforms = filter(p -> Sys.islinux(p) || Sys.isfreebsd(p), platforms) From 1b5c89903ac37631959d24f58b27c1cd20d31bc9 Mon Sep 17 00:00:00 2001 From: Clem Cords Date: Wed, 28 Feb 2024 15:35:08 +0100 Subject: [PATCH 30/31] removed glmakie integration --- Project.toml | 4 +- README.md | 17 -- docs/src/01_manual/12_opengl_integration.md | 293 +------------------- 3 files changed, 4 insertions(+), 310 deletions(-) diff --git a/Project.toml b/Project.toml index 5e9e482..0d3c729 100644 --- a/Project.toml +++ b/Project.toml @@ -18,7 +18,7 @@ libcxxwrap_julia_jll = "3eaa8342-bff7-56a5-9981-c04077f7cee7" mousetrap_jll = "0e90efc8-2bbd-550f-bf3c-306a2edaaeef" [compat] -CxxWrap = "^0.14.0" -mousetrap_jll = "^0.3.0" +CxxWrap = "0.14, 0.15" +mousetrap_jll = "0.3.0" GTK4_jll = "4.10" Glib_jll = "2.76" \ No newline at end of file diff --git a/README.md b/README.md index baf2af0..5367800 100644 --- a/README.md +++ b/README.md @@ -101,23 +101,6 @@ add_render_task!(render_area, RenderTask(rectangle)) --- -### Displaying a GLMakie Plot in a Mousetrap Window - -```julia -using GLMakie, MousetrapMakie -canvas = GLMakieArea() -window = Mousetrap.Window() -set_child!(window, canvas) # can be used like any other widget - -screen = create_glmakie_screen(canvas) -display(screen, scatter(rand(123))) -``` -![](docs/src/assets/makie_scatter.png) - -(**Note**: This feature is still experimental. See [here](https://github.com/Clemapfel/MousetrapMakie.jl) for more information) - ---- - ## Supported Platforms Since `v0.3.0`, Mousetrap is fully portable. All features are available for all 64-bit versions of Linux, FreeBSD, macOS, and Windows. diff --git a/docs/src/01_manual/12_opengl_integration.md b/docs/src/01_manual/12_opengl_integration.md index 275bebf..ab459dc 100644 --- a/docs/src/01_manual/12_opengl_integration.md +++ b/docs/src/01_manual/12_opengl_integration.md @@ -124,294 +124,5 @@ end We see that we should make sure to bind the context using `make_current` before doing any OpenGL-related work and to manually request a redraw after the area was initialized or resized. -# Example: GLMakie - -[`GLMakie`](https://docs.makie.org/stable/explanations/backends/glmakie/index.html) is one backend for the hugely popular [`Makie`](https://github.com/MakieOrg/Makie.jl) plotting library. As its name suggests, `GLMakie` uses OpenGL for rendering, which means it is possible to allow Makie to render to a Mousetrap `GLArea`, allowing us to integrate plots and graphics into our Mousetrap application. - -Given here will be a minimum working example that displays a scatter plot inside a `Mousetrap.Window` by creating a `GLArea`-based widget that can be used to create a `GLMakie.Screen`. - -Note that this example is incomplete and does not support all of Makies features. One conflict that Mousetrap users will have to resolve for themselves is how to handle input events. In the following, all of Makies input-related behavior was suppressed, making it so users will have to handle input events and window behavior using only the Mousetrap event model. - -!!! details "Note from the Author: Makie Interface" - The example here most likely does not implement enough of Makies interface to be fully ready for usage. Most of the code was based on [`Gtk4GLMakie`](https://github.com/JuliaGtk/Gtk4Makie.jl), which itself is still rough. I'm not that familiar with Makie in general usage, and fully implementing an interface requires knowledge of Makies internals on top of that. - - If you or your project is very familiar with Makie and would like to improve this code, feel free to [open a PR](https://github.com/Clemapfel/Mousetrap.jl/pulls) that modifies [`test/makie_test.jl`](https://github.com/Clemapfel/Mousetrap.jl/blob/main/test/makie_test.jl), which ideally will become its own Julia package in the future, similar to `Gtk4GLMakie`. Any contributor will be credited as an author. Thank you for your consideration. - - C. - -!!! details "MousetrapMakie: Click to expand" - ```julia - """ - Minimum working example showing how to display a GLMakie plot using Mousetrap `GLArea` - """ - module MousetrapMakie - - export GLMakieArea, create_glmakie_screen - - using Mousetrap - using ModernGL, GLMakie, Colors, GeometryBasics, ShaderAbstractions - using GLMakie: empty_postprocessor, fxaa_postprocessor, OIT_postprocessor, to_screen_postprocessor - using GLMakie.GLAbstraction - using GLMakie.Makie - - """ - ## GLMakieArea <: Widget - `GLArea` wrapper that automatically connects all necessary callbacks to be used as a GLMakie render target. - - Use `create_glmakie_screen` to initialize a screen you can render to using Makie from this widget. Note that `create_glmakie_screen` needs to be - called **after** `GLMakieArea` has been realized, as only then will the internal OpenGL context be available. See the example below. - - ## Constructors - `GLMakieArea()` - - ## Signals - (no unique signals) - - ## Fields - (no public fields) - - ## Example - - using Mousetrap, MousetrapMakie - main() do app::Application - window = Window(app) - canvas = GLMakieArea() - set_size_request!(canvas, Vector2f(200, 200)) - set_child!(window, canvas) - - # use optional ref to delay screen allocation after `realize` - screen = Ref{Union{Nothing, GLMakie.Screen{GLMakieArea}}}(nothing) - connect_signal_realize!(canvas) do self - screen[] = create_glmakie_screen(canvas) - display(screen[], scatter(1:4)) - return nothing - end - present!(window) - end - """ - mutable struct GLMakieArea <: Widget - - glarea::GLArea # wrapped native widget - framebuffer_id::Ref{Int} # set by render callback, used in MousetrapMakie.create_glmakie_screen - framebuffer_size::Vector2i # set by resize callback, used in GLMakie.framebuffer_size - - function GLMakieArea() - glarea = GLArea() - set_auto_render!(glarea, false) # should `render` be emitted everytime the widget is drawn - connect_signal_render!(on_makie_area_render, glarea) - connect_signal_resize!(on_makie_area_resize, glarea) - return new(glarea, Ref{Int}(0), Vector2i(0, 0)) - end - end - Mousetrap.get_top_level_widget(x::GLMakieArea) = x.glarea - - # maps hash(GLMakieArea) to GLMakie.Screen - const screens = Dict{UInt64, GLMakie.Screen}() - - # maps hash(GLMakieArea) to Scene, used in `on_makie_area_resize` - const scenes = Dict{UInt64, GLMakie.Scene}() - - # render callback: if screen is open, render frame to `GLMakieArea`s OpenGL context - function on_makie_area_render(self, context) - key = Base.hash(self) - if haskey(screens, key) - screen = screens[key] - if !isopen(screen) return false end - screen.render_tick[] = nothing - glarea = screen.glscreen - glarea.framebuffer_id[] = glGetIntegerv(GL_FRAMEBUFFER_BINDING) - GLMakie.render_frame(screen) - end - return true - end - - # resize callback: update framebuffer size, necessary for `GLMakie.framebuffer_size` - function on_makie_area_resize(self, w, h) - key = Base.hash(self) - if haskey(screens, key) - screen = screens[key] - glarea = screen.glscreen - glarea.framebuffer_size.x = w - glarea.framebuffer_size.y = h - queue_render(glarea.glarea) - end - - if haskey(scenes, key) - scene = scenes[key] - scene.events.window_area[] = Recti(0, 0, glarea.framebuffer_size.x, glarea.framebuffer_size.y) - scene.events.window_dpi[] = Mousetrap.calculate_monitor_dpi(glarea) - end - return nothing - end - - # resolution of `GLMakieArea` OpenGL framebuffer - GLMakie.framebuffer_size(self::GLMakieArea) = (self.framebuffer_size.x, self.framebuffer_size.y) - - # forward retina scale factor from GTK4 back-end - GLMakie.retina_scaling_factor(w::GLMakieArea) = Mousetrap.get_scale_factor(w) - - # resolution of `GLMakieArea` widget itself` - function GLMakie.window_size(w::GLMakieArea) - size = get_natural_size(w) - size.x = size.x * GLMakie.retina_scaling_factor(w) - size.y = size.y * GLMakie.retina_scaling_factor(w) - return (size.x, size.y) - end - - # calculate screen size and dpi - function Makie.window_area(scene::Scene, screen::GLMakie.Screen{GLMakieArea}) - glarea = screen.glscreen - scenes[hash(glarea)] = scene - end - - # resize request by makie will be ignored - function GLMakie.resize_native!(native::GLMakieArea, resolution...) - # noop - end - - # bind `GLMakieArea` OpenGL context - ShaderAbstractions.native_switch_context!(a::GLMakieArea) = make_current(a.glarea) - - # check if `GLMakieArea` OpenGL context is still valid, it is while `GLMakieArea` widget stays realized - ShaderAbstractions.native_context_alive(x::GLMakieArea) = get_is_realized(x) - - # destruction callback ignored, lifetime is managed by Mousetrap instead - function GLMakie.destroy!(w::GLMakieArea) - # noop - end - - # check if canvas is still realized - GLMakie.was_destroyed(window::GLMakieArea) = !get_is_realized(window) - - # check if canvas should signal it is open - Base.isopen(w::GLMakieArea) = !GLMakie.was_destroyed(w) - - # react to makie screen visibility request - GLMakie.set_screen_visibility!(screen::GLMakieArea, bool) = bool ? show(screen.glarea) : hide!(screen.glarea) - - # apply glmakie config - function GLMakie.apply_config!(screen::GLMakie.Screen{GLMakieArea}, config::GLMakie.ScreenConfig; start_renderloop=true) - @warn "In MousetrapMakie: GLMakie.apply_config!: This feature is not yet implemented, ignoring config" - # cf https://github.com/JuliaGtk/Gtk4Makie.jl/blob/main/src/screen.jl#L111 - return screen - end - - # screenshot framebuffer - function Makie.colorbuffer(screen::GLMakie.Screen{GLMakieArea}, format::Makie.ImageStorageFormat = Makie.JuliaNative) - @warn "In MousetrapMakie: GLMakie.colorbuffer: This feature is not yet implemented, returning framecache" - # cf https://github.com/JuliaGtk/Gtk4Makie.jl/blob/main/src/screen.jl#L147 - return screen.framecache - end - - # ignore makie event model, use Mousetrap event controllers instead - Makie.window_open(::Scene, ::GLMakieArea) = nothing - Makie.disconnect!(::GLMakieArea, f) = nothing - GLMakie.pollevents(::GLMakie.Screen{GLMakieArea}) = nothing - Makie.mouse_buttons(::Scene, ::GLMakieArea) = nothing - Makie.keyboard_buttons(::Scene, ::GLMakieArea) = nothing - Makie.dropped_files(::Scene, ::GLMakieArea) = nothing - Makie.unicode_input(::Scene, ::GLMakieArea) = nothing - Makie.mouse_position(::Scene, ::GLMakie.Screen{GLMakieArea}) = nothing - Makie.scroll(::Scene, ::GLMakieArea) = nothing - Makie.hasfocus(::Scene, ::GLMakieArea) = nothing - Makie.entered_window(::Scene, ::GLMakieArea) = nothing - - """ - - create_gl_makie_screen(::GLMakieArea; screen_config...) -> GLMakie.Screen{GLMakieArea} - - For a `GLMakieArea`, create a `GLMakie.Screen` that can be used to display makie graphics - """ - function create_glmakie_screen(area::GLMakieArea; screen_config...) - - if !get_is_realized(area) - log_critical("MousetrapMakie", "In MousetrapMakie.create_glmakie_screen: GLMakieArea is not yet realized, it's internal OpenGL context cannot yet be accessed") - end - - config = Makie.merge_screen_config(GLMakie.ScreenConfig, screen_config) - - set_is_visible!(area, config.visible) - set_expand!(area, true) - - # quote from https://github.com/JuliaGtk/Gtk4Makie.jl/blob/main/src/screen.jl#L342 - shader_cache = GLAbstraction.ShaderCache(area) - ShaderAbstractions.switch_context!(area) - fb = GLMakie.GLFramebuffer((1, 1)) # resized on GLMakieArea realization later - - postprocessors = [ - config.ssao ? ssao_postprocessor(fb, shader_cache) : empty_postprocessor(), - OIT_postprocessor(fb, shader_cache), - config.fxaa ? fxaa_postprocessor(fb, shader_cache) : empty_postprocessor(), - to_screen_postprocessor(fb, shader_cache, area.framebuffer_id) - ] - - screen = GLMakie.Screen( - area, shader_cache, fb, - config, false, - nothing, - Dict{WeakRef, GLMakie.ScreenID}(), - GLMakie.ScreenArea[], - Tuple{GLMakie.ZIndex, GLMakie.ScreenID, GLMakie.RenderObject}[], - postprocessors, - Dict{UInt64, GLMakie.RenderObject}(), - Dict{UInt32, Makie.AbstractPlot}(), - false, - ) - # end quote - - hash = Base.hash(area.glarea) - screens[hash] = screen - - set_tick_callback!(area.glarea) do clock::FrameClock - if GLMakie.requires_update(screen) - queue_render(area.glarea) - end - - if GLMakie.was_destroyed(area) - return TICK_CALLBACK_RESULT_DISCONTINUE - else - return TICK_CALLBACK_RESULT_CONTINUE - end - end - return screen - end - end - ``` - -We can test the above using: - -```julia -using Mousetrap, .MousetrapMakie, GLMakie -main() do app::Application - window = Window(app) - set_title!(window, "Mousetrap x Makie") - - canvas = GLMakieArea() - set_size_request!(canvas, Vector2f(200, 200)) - set_child!(window, canvas) - - # use optional ref to delay screen allocation after `realize` - screen = Ref{Union{Nothing, GLMakie.Screen{GLMakieArea}}}(nothing) - connect_signal_realize!(canvas) do self - - # initialize GLMakie.Screen - screen[] = create_glmakie_screen(canvas) - - # use screen to display plot - display(screen[], scatter(rand(123))) - return nothing - end - - present!(window) -end -``` - -![](../assets/makie_scatter.png) - -Where we delayed the call to `create_gl_makie_screen` to *after* `realize` was emitted for reasons discussed [earlier in this chapter](#glarea). Since we still need to reference the created screen outside the `realize` signal handler, we used the **optional pattern**: - -```julia -optional = Ref{Union{Nothing, T}}(nothing) -``` - -Which initializes a reference with `nothing`, such that the reference value can later be assigned with a value of `T`, `GLMakie:Screen{GLMakieArea}` in our example above. After `realize` was emitted, we can access the screen using `screen[]`. +!!! danger + February, 2024: This section used to contain an example of how to get `GLMakie` to work with `Mousetrap.GLArea`. Recent changes to the internals of `GLMakie` have broken the old package, so it no longer works. `MousetrapMakie` is now unmaintained. \ No newline at end of file From a3518cd033fc7ae78801d17f4d32d43c92d40cb0 Mon Sep 17 00:00:00 2001 From: Clem Cords Date: Wed, 28 Feb 2024 15:54:52 +0100 Subject: [PATCH 31/31] Update 12_opengl_integration.md --- docs/src/01_manual/12_opengl_integration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/01_manual/12_opengl_integration.md b/docs/src/01_manual/12_opengl_integration.md index ab459dc..73f5e7f 100644 --- a/docs/src/01_manual/12_opengl_integration.md +++ b/docs/src/01_manual/12_opengl_integration.md @@ -125,4 +125,4 @@ end We see that we should make sure to bind the context using `make_current` before doing any OpenGL-related work and to manually request a redraw after the area was initialized or resized. !!! danger - February, 2024: This section used to contain an example of how to get `GLMakie` to work with `Mousetrap.GLArea`. Recent changes to the internals of `GLMakie` have broken the old package, so it no longer works. `MousetrapMakie` is now unmaintained. \ No newline at end of file + February, 2024: This section used to contain an example of how to get `GLMakie` to work with `Mousetrap.GLArea`. Recent changes to the internals of `GLMakie` have broken this example, and the corresponding experimental package `MousetrapMakie`. Both no longer work, and `MousetrapMakie` is now unmaintained. \ No newline at end of file