Cards and widgets
This page is for package authors and developers who add a component or a GUI extension to live_view. It shows how a new type gets its own card in the 3D view (and in the "Properties" sidebar of layout = :app), how an own widget type is used on cards and in controls, and how parameters without a scene object get their own controls. Everything works identically in layout = :compact and layout = :app. The user-facing description of the window is on the page Live view.
All code blocks assume using BeamletOptics, BeamletOpticsGUI, GLMakie, need a Makie backend with a window (GLMakie) and are therefore not run when the docs are built. Unless a recipe names another test file, they are run as tests in test/TestLiveWidgetRecipe.jl of BeamletOpticsGUI, from which they are copied unchanged.
Which path?
| I want to add ... | path | function |
|---|---|---|
widgets for something in the scene: a component, a group, a source, a system, an own AbstractObject subtype | a card | card_rows, card_actions |
| a parameter without a scene object, e.g. a setting of the whole setup | a controls section | add_controls! |
| an action, e.g. a button or a toggle with a key | a tool | add_tool! |
| a plot or a result that is updated after each solve | a panel | add_panel! |
A card needs a scene object because the object gives the card its anchor (the card is placed next to the bounding box of the object and follows it), its selection (the card shows what is selected, clicked in the menu or in the tree), its pin (the card stays with its object) and its row in the object tree. A parameter without such an object has none of these, hence it belongs into a controls section.
A card from a package extension
The card functions belong to BeamletOpticsGUI, not to BeamletOptics. A package that defines its own component types and wants cards for them does not depend on BeamletOpticsGUI: it declares BeamletOpticsGUI as a weak dependency and puts its card_rows and card_actions methods into a package extension, which is loaded when the user loads BeamletOpticsGUI:
# Project.toml of MyOptics:
# [weakdeps] BeamletOpticsGUI = "<uuid of BeamletOpticsGUI>"
# [extensions] MyOpticsGUIExt = "BeamletOpticsGUI"
# ext/MyOpticsGUIExt.jl
module MyOpticsGUIExt
using MyOptics, BeamletOptics, BeamletOpticsGUI
BeamletOpticsGUI.card_rows(x::MyOptics.MyType) = (pose_card_rows(x)..., CardRow("n", "1.5"))
endThe recipes below define the methods directly in a script, which works the same way.
Recipe: a card for your type
The card of a selected object shows the rows of card_rows below its head with the actions of card_actions, and, only on the card of the selection, the step, the Move/Rotate control and the page "Properties", which the live view adds. The rows are the page "Pose" of the card. To add rows for an own type:
- Define a method
BeamletOpticsGUI.card_rows(x::MyType). For a movable object, start frompose_card_rows(x)...(pose_card_rows), which gives the position and rotation boxes. An own beam type addsbeam_card_rows(x)...(beam_card_rows), the toggle that switches it off and on and, for polarized beams, the polarization toggle. - Return one
CardRowper line. Its cells are strings (labels) orCardWidgets. value(gui, obj)returns what the widget shows, in display units (mm, mrad, %), and must be cheap: it runs after every move, solve and input.on(gui, obj, v)applies an inputvtoobjin SI units. Setsolve = trueif the input changes the optics: a running solve is cancelled first and the systems are solved again after the input.- Give a widget a
nameto find it in tests. Extra actions in the head of the card are added by a method ofcard_actions, which extends the default ones (the eye that hides the object). - Keep no state in the widgets and capture no
gui: the card may be rebuilt at any time and shows other objects with the same declarations.valueandonget theguiand the object. - Test it: create a
live_viewwith the object, select it withgui.controls.selected[] = obj, look up the widget by itsnameon the card, set an input and check the object, the shown value and that a solve followed. The tests of the recipes do this for both layouts, seetest/TestLiveWidgetRecipe.jlof BeamletOpticsGUI.
The example is a filter with a transmission, whose card has a slider (its value is shown in %) and an action "block" next to the eye:
# A filter that passes the rays and attenuates their power by `transmission` (0-1)
mutable struct MyAttenuator{T} <: BeamletOptics.AbstractObject{T}
shape::BeamletOptics.PlanoSurfaceSDF{T}
transmission::Float64
end
MyAttenuator(diameter, thickness; transmission = 0.5) =
MyAttenuator(BeamletOptics.PlanoSurfaceSDF(thickness, diameter), Float64(transmission))
# The rays pass straight through (the power is not traced in this example)
function BeamletOptics.interact3d(::BeamletOptics.AbstractSystem, a::MyAttenuator,
::Beam{T, R}, ray::R) where {T <: Real, R <: Ray{T}}
pos = position(ray) + length(ray) * direction(ray)
# entering: the next surface is the back of the filter
hint = BeamletOptics.isentering(ray) ? BeamletOptics.Hint(a) : nothing
new = Ray{T}(pos, direction(ray), nothing, BeamletOptics.wavelength(ray), BeamletOptics.refractive_index(ray))
return BeamletOptics.BeamInteraction{T, R}(hint, new)
end
# The card: the pose, a slider for the transmission with its value in %, and an action "block"
# next to the default ones (the eye)
BeamletOpticsGUI.card_rows(a::MyAttenuator) = (pose_card_rows(a)...,
CardRow("T",
CardWidget(Slider; name = :transmission, range = 0:0.01:1, width = 150,
value = (gui, a) -> a.transmission,
on = (gui, a, v) -> (a.transmission = v), solve = true),
CardWidget(Label; name = :percent,
value = (gui, a) -> "$(round(Int, 100 * a.transmission)) %")))
BeamletOpticsGUI.card_actions(a::MyAttenuator) = (
invoke(BeamletOpticsGUI.card_actions, Tuple{BeamletOptics.AbstractObject}, a)...,
CardWidget(Button; name = :block, label = "block",
on = (gui, a, _) -> (a.transmission = 0.0), solve = true))The types of BeamletOptics have such methods as well, e.g. the ray count slider of a source or the signal of a Detector.
Recipe: a card for your system type
A system (a subtype of AbstractSystem) has a card as well: it is shown, without a gizmo and without a selection (gui.controls.selected[] stays nothing), when its entry in the component menu (layout = :compact) or its row in the object tree (layout = :app) is clicked, and closed by Esc, a click on empty space or the selection of an object. It is the system widget of the window (its name with a pencil, its tracing, "+" and "−", the list of its members and "Remove system", see Several systems); a system has no rows of its own by default. The rows of your card_rows method are shown below the list of the members. The steps are those of the card of a type, except that a system has no pose:
# A system with a name, traced like a `System`
struct MyBench <: BeamletOptics.AbstractSystem
objects::Vector{BeamletOptics.ObjectOrGroup}
name::String
end
BeamletOptics.objects(b::MyBench) = b.objects
# The name of the bench, below the list of its members (a system has no rows by default)
BeamletOpticsGUI.card_rows(b::MyBench) = (
invoke(BeamletOpticsGUI.card_rows, Tuple{BeamletOptics.AbstractSystem}, b)...,
CardRow("bench", CardWidget(Label; name = :bench, value = (gui, b) -> b.name)))An object that is not movable is shown on its card in the same way (inspected instead of selected), its pose boxes reject inputs with a message in the status line.
The parts of a group: the selection card
In live_view, a click on an object of a group opens the selection card of its top-level group instead of selecting it. The card browses the parts one level at a time: the first entry "Select <group>" selects the group itself for moving, "‹ <parent>" (below the top level) browses the enclosing object, then come the direct parts, marked " ›" if a part has parts of its own (it opens the next level; any other part is selected). A part of an object that is not a group, e.g. a lens of a doublet, is shown but not movable: its pose boxes reject inputs. While browsing, the group is drawn see-through with a box around each part, and the box of the hovered entry is highlighted. A click in the 3D view on a part acts like its entry, a click beside closes the card, Esc goes up one level (closes at the top level). The card of a part has a button "‹" in its head that browses its parent. See the section "Selection card" of live_view.
A group is an ObjectOrGroup container, not a MultiShape object (BeamletOptics 0.14): an own group type is a mutable struct ... <: BeamletOptics.AbstractObjectGroup{T} with the fields objects (its parts), center (the pivot) and dir (the orientation), see AbstractObjectGroup of BeamletOptics. Its card needs no code: the parts come from BeamletOptics.objects, and card_rows needs no row for them.
Recipe: a card for an object without a place in the scene
The keyword background_card of live_view shows the card of an object that has no place in the scene, e.g. the settings of an environment, on a click on the empty background while nothing is selected (with a selection, the click only deselects). It is the object itself or a function gui -> object or nothing, which is evaluated at each such click and may return nothing for no card, or object => point to attach the card to the point [m], e.g. where the ray through the mouse (Makie.ray_at_cursor) meets a sky dome, instead of at the click. The card has the rows of card_rows of the object (pose rows only if the method adds them), no actions and the title of the labels entry of the object (else its type); value(gui, obj) and on(gui, obj, v) get the object itself. For a parameter that is meant to be always visible use add_controls! instead. The example is abridged from test/TestLiveBackgroundCard.jl:
# Not a component: e.g. the environment of a telescope, without pose or shape
mutable struct Sky
hour::Float64
end
BeamletOpticsGUI.card_rows(::Sky) = (CardRow("hour",
CardWidget(Slider; name = :hour, range = 0:0.5:24, width = 120,
value = (gui, s) -> s.hour, on = (gui, s, v) -> (s.hour = v)),
CardWidget(Label; name = :hour_text, value = (gui, s) -> "$(s.hour) h")),)
sky = Sky(6.0)
gui = live_view(system, beam; background_card = sky, labels = Dict(sky => "Sky"))Recipe: your own widget type
Any Makie block can be a CardWidget and is then placed, hidden and refreshed by the card. An own widget type, e.g. a Makie.@Block or another type that is constructed as T(position; attributes...) and places itself at a GridPosition, takes part by two methods:
card_input(w)returns theObservablewhose updates are the inputs of the user. Each update is passed asvto theon(gui, obj, v)of the declaration. Returnnothing(the default) if the widget takes no input.card_show!(w, v)shows the valuevofvalue(gui, obj). Showing a value is not an input: the card ignores the updates ofcard_inputwhile it shows values, but nothing does outside of a card, e.g. inadd_controls!. Hence do not change the observable ofcard_inputincard_show!, and keep what the user is typing (the method for aTextboxdoes not change a focused box unlessforce = true).
An own widget of several blocks must delete them in Base.delete!, which the card calls when it rebuilds its widgets; a Makie.@Block with @forwarded_layout does it by itself. Makie.@Block outside of Makie needs the name make_block_docstring of Makie in scope. The example is a value with a "−" and a "+" button, first on the card of a block, then in a controls section:
# `Makie.@Block` outside of Makie needs this name of Makie
using Makie: make_block_docstring
# A value with a "−" and a "+" button, which step it by `step`
Makie.@Block Stepper begin
@forwarded_layout
minus::Button
plus::Button
@attributes begin
"The value shown, see `card_show!`."
value::Float64 = 0.0
"The value requested by the last click, see `card_input`."
input::Float64 = 0.0
"The change of the value per click."
step = 1.0
"The horizontal alignment of the block in its suggested bounding box."
halign = :center
"The vertical alignment of the block in its suggested bounding box."
valign = :center
"The width setting of the block."
width = Auto()
"The height setting of the block."
height = Auto()
"Controls if the parent layout can adjust to this block's width."
tellwidth::Bool = true
"Controls if the parent layout can adjust to this block's height."
tellheight::Bool = true
"The align mode of the block in its parent GridLayout."
alignmode = Inside()
end
end
function Makie.initialize_block!(s::Stepper)
s.minus = Button(s.layout[1, 1]; label = "−")
Label(s.layout[1, 2], lift(v -> string(round(v; digits = 3)), s.value); width = 50)
s.plus = Button(s.layout[1, 3]; label = "+")
# a click is an input: the value one step away from the shown one
on(_ -> (s.input[] = s.value[] - s.step[]), s.minus.clicks)
on(_ -> (s.input[] = s.value[] + s.step[]), s.plus.clicks)
return nothing
end
# The protocol of the widgets of the cards: the inputs, and showing a value (not an input)
BeamletOpticsGUI.card_input(s::Stepper) = s.input
BeamletOpticsGUI.card_show!(s::Stepper, v) = (s.value[] = v; nothing)
# A beam block, which its card moves up and down (along z) in steps
struct MyBeamBlock{T} <: BeamletOptics.AbstractObject{T}
shape::BeamletOptics.PlanoSurfaceSDF{T}
end
MyBeamBlock(width, thickness) = MyBeamBlock(BeamletOptics.PlanoSurfaceSDF(thickness, width))
# Absorbs the rays that hit it
BeamletOptics.interact3d(::BeamletOptics.AbstractSystem, ::MyBeamBlock, ::Beam, ::Ray) = nothing
BeamletOpticsGUI.card_rows(b::MyBeamBlock) = (CardRow("z [mm]",
CardWidget(Stepper; name = :height, step = 15.0,
value = (gui, b) -> 1e3 * position(b)[3],
on = (gui, b, z) -> translate_to3d!(b, [position(b)[1], position(b)[2], 1e-3 * z]),
solve = true)),)
# The same widget in a controls section, for the block: shows its position after each solve
function add_block_controls!(gui, block)
add_controls!(gui, "Beam block") do layout
Label(layout[1, 1], "z [mm]")
stepper = Stepper(layout[1, 2]; step = 15.0)
on(card_input(stepper)) do z
retrace!(() -> translate_to3d!(block, [position(block)[1], position(block)[2], 1e-3 * z]), gui)
end
return gui -> card_show!(stepper, 1e3 * position(block)[3])
end
endRecipe: controls without a scene object
A parameter that belongs to no object, e.g. an alignment setting of the whole setup, has no card. It is a controls section:
add_controls!(gui, title) do layout ... endbuildsMakieblocks into the givenlayout. The layout places the section (an entry of the tool rail that opens a popover, or a section of the left sidebar); never use fixedgui.fig[...]positions.- A
TextboxorMenubuilt insideftakes the keyboard automatically, i.e. the keys of the 3D view are ignored while it is focused or open. Blocks added afterfreturned do not. - Every change of the optics goes through
retrace!(f, gui)(retrace!), which cancels a running solve first and makes the changefin its place. Never change objects outsidef. - Return
update(gui)from the builder to show the current state after each full solve (and once right away), e.g. after an undo or a change from code.updatemust not change objects. A builder that ends with any other value, including theon(...)of a listener, has noupdate. - Errors in callbacks are logged once and do not stop the view.
- It works identically in
:compactand:app; own widget types usecard_inputandcard_show!here too, as in the example above.
The example is a misalignment scenario, set with a textbox and switched on and off with a toggle. It acts on the fold mirror, but it is a setting of the setup, not a property of the mirror:
# A misalignment scenario of the setup, not a property of an object: it tilts the fold mirror about z;
# the tilt [rad] and whether it is applied
const tilt = Ref(0.0)
const tilted = Ref(false)
function set_tilt!(gui, mirror, θ, on::Bool)
retrace!(gui) do
# the rotation from the applied tilt to the new one
zrotate3d!(mirror, (on ? θ : 0.0) - (tilted[] ? tilt[] : 0.0))
tilt[], tilted[] = θ, on
end
return nothing
end
function add_tilt_controls!(gui, mirror)
add_controls!(gui, "Alignment") do layout
Label(layout[1, 1], "tilt [mrad]")
box = Textbox(layout[1, 2]; placeholder = "0", width = 80, validator = Float64)
toggle = Toggle(layout[1, 3])
Label(layout[1, 4], "tilted")
on(s -> set_tilt!(gui, mirror, 1e-3 * parse(Float64, s), toggle.active[]), box.stored_string)
on(v -> v == tilted[] || set_tilt!(gui, mirror, tilt[], v), toggle.active)
# the parameter as it is, e.g. after it was set from code
return gui -> (card_show!(box, 1e3 * tilt[]); card_show!(toggle, tilted[]))
end
endChecklist for a new component
interact3d(system, object, beam, ray): required, see Core design page of BeamletOptics.BeamletOptics.initialize!(object): only ifinteract3dstores data in the component, e.g. the hits of an own detector. It discards them; the live view calls it before every solve.render!: optional, how the component is drawn.card_rows(andcard_actions): optional, its rows and buttons on the card of the live view.BeamletOptics.properties: optional, the properties listed on the page "Properties" of the card and of the inspector, seeproperties.
Reference
The functions and types of the recipes:
BeamletOpticsGUI.card_rows — Function
card_rows(obj)Rows of the card of obj in live_view, a tuple of CardRows, chosen by multiple dispatch. By default, the rows of the pose, see pose_card_rows; beams, beam groups and sources add the rows of beam_card_rows (toggles and the drawn length of the final rays), a source whose rays can be regenerated (see BeamletOptics.set_num_rays!) a slider for the number of rays, a Detector its signal (the power or the number of rays of its detector view while a view is shown, else the number of hits), mechanics (NonInteractableObject, e.g. a NonInteractableObject(loadpath) of an STL file, and IntersectableObject) a slider for their opacity. A system (AbstractSystem) has no rows by default: its card (shown after a click on its entry in the component menu or the object tree) is the system widget of the window (its tracing, its members and "Remove system", see "Systems" of live_view), which is not part of this method; the rows of a method for an own system type are shown below the list of its members. Add a method for an own type to show its properties or controls on its card, e.g.
BeamletOpticsGUI.card_rows(l::MyLens) = (pose_card_rows(l)...,
CardRow("f", CardWidget(Label; value = (gui, l) -> "$(1e3 * focal_length(l)) mm")))A card exists only for things in the scene (objects, groups, sources, systems, clip planes); a parameter without an object belongs into add_controls!. The card may be rebuilt at any time and shows other objects of the same declarations, hence the widgets keep no state: value and on get the gui and the object. value runs after every move, solve and input and must be cheap. The rows of a system type extend the default ones via invoke(BeamletOpticsGUI.card_rows, Tuple{BeamletOptics.AbstractSystem}, sys).
BeamletOpticsGUI has the methods for the types of BeamletOptics.
BeamletOpticsGUI.pose_card_rows — Function
pose_card_rows(obj)The rows of the pose of obj on its card in live_view, see card_rows: the position x, y, z [mm], where Enter moves obj to the typed coordinate, and the rotations rx, ry, rv [mrad] about the red, green and blue axis of the controls.
BeamletOpticsGUI.beam_card_rows — Function
beam_card_rows(beam)The rows of the beam on the card of a beam, beam group or source beam in live_view, see card_rows. The row beam has the toggle "on", which switches the beam off and on. A beam that is off is neither solved nor drawn, its rays are removed, such that detectors and measurements ignore it, and only its source marker stays; with auto_trace = false the switch marks the beams as outdated instead of solving.
Gaussian beamlets (GaussianBeamlet, AstigmaticGaussianBeamlet and groups of them) add the toggle "beams", which draws the generating beams (chief, divergence and waist rays) as the static render! with show_beams = true does. Polarized beams (their rays are PolarizedRays, e.g. an AstigmaticGaussianBeamlet) add the toggle "polarization", which draws the polarization along the beam, and the rows pol λ and pol amp with sliders for the wavelength [mm] of the drawn curve and its amplitude ([mm], of astigmatic beamlets as a multiple of the beam radius), on logarithmic scales over ranges given by the size of the scene. Of a beam group, the generating beams and the polarization are drawn for its central beam, the one starting nearest the axis of the group. These change the display only. The initial states are set by the live_view keywords beams_off and beam_kwargs (show_beams, show_polarization, pol_λ, pol_amplitude, pol_scale).
The last row, length, has a box with the length [mm] with which the final rays of the beam, i.e. those that hit nothing, are drawn (flen of live_render!: by default 1 m, of Gaussian beamlets 0.1 m, or as given by the beam_kwargs). An input changes the drawn length of the beam and of its overlays in place, nothing is solved; an input that is no positive number is reported in the status line.
The card of an own beam type adds the rows after its own rows, e.g.
BeamletOpticsGUI.card_rows(b::MyBeam) = (pose_card_rows(b)..., beam_card_rows(b)...)BeamletOpticsGUI.card_actions — Function
card_actions(obj)Widgets in the head of the card of obj in live_view, a tuple of CardWidgets, chosen by multiple dispatch like card_rows: by default the action named :hide, an eye icon that hides the object and, crossed out for a hidden object, shows it again (tooltips "Hide" and "Show"); for clip planes the buttons "flip" and "remove"; for systems the eye and :open, an icon of two windows that opens the system in a window of its own (see open_system). The card of an object with parts (a group or a MultiShape object) gets the button "parts ›" after them, which opens its selection card.
A method for an own type extends the default ones, e.g. (invoke(card_actions, Tuple{Any}, obj)..., CardWidget(Button; label = "block", on = ...)).
BeamletOpticsGUI.CardRow — Type
CardRow(cells...)A line of a card of live_view, see card_rows: its cells from left to right, each a text or a CardWidget.
BeamletOpticsGUI.CardWidget — Type
CardWidget(T; name = nothing, value = nothing, on = nothing, solve = false, attributes...)A widget on a card of live_view, see card_rows: a Makie block of the type T, e.g. Label, Slider, Toggle, Textbox, Button or Menu, created as T(position; attributes...) with the attributes, e.g. the range of a Slider or the width. The card places the widget, hides it with the card and keeps clicks on it from the 3D view, and its textboxes take the keyboard. T may be an own widget type with methods of card_input and card_show!, e.g. a Makie.@Block.
value(gui, obj): the value the widget shows for the objectobjof the card, passed tocard_show!: the text of aLabelorButton, the value of aSlider,ToggleorTextbox. Refreshed when the card gets its object, after moves, solves and inputs.on(gui, obj, v): applies an inputvof the widget toobj, a new value of itscard_input: the value of aSlider, the state of aToggle, the entered text of aTextbox, the click count of aButtonor the selection of aMenu. Inputs of a widget withoutonare ignored.solve = true: the input changes the optics, e.g. a parameter ofobj: a running solve is cancelled beforeon, and the systems are solved again afterwards (or the beams are marked as outdated without auto tracing), like after a move, via theon_changeof the controls.name: finds the widget on a card, e.g. in tests.
gui is the LiveView, e.g. for the beams of the last solve or the status line.
BeamletOpticsGUI.card_input — Function
card_input(w) -> Union{Observable, Nothing}The input of the widget w on a card of live_view (see CardWidget): an Observable whose updates are the inputs of the user, each passed as v to the on(gui, obj, v) of the declaration. nothing means that the widget takes no input, e.g. a Label, and is the default for any type. BeamletOpticsGUI has methods for the blocks of Makie: the value of a Slider, active of a Toggle, stored_string of a Textbox, clicks of a Button and selection of a Menu.
An own widget type, e.g. a Makie.@Block or any type constructed as T(position; attributes...) that places itself at a GridPosition, adds a method of card_input and of card_show! and can then be declared as CardWidget(T; ...) on a card. Widgets built in add_controls! may use both functions as well, the controls do not call them. An own type that consists of several blocks deletes them in Base.delete!, which the card calls when it rebuilds its widgets (a Makie.@Block with @forwarded_layout does so by itself).
BeamletOpticsGUI.card_show! — Function
card_show!(w, v)Shows the value v in the widget w on a card of live_view, where v is the result of the value(gui, obj) of its CardWidget. The card calls it when it gets its object and after moves, solves and inputs. BeamletOpticsGUI has methods for the blocks of Makie: the text of a Label or the label of a Button (string(v)), the text of a Textbox, the value of a Slider (the closest step of its range), the state of a Toggle and the selected option of a Menu (the option with the value v; an unknown value keeps the selection). The default for any other type shows nothing.
Showing a value is not an input: the card ignores the updates of card_input while it shows values, and a method should not change the observable of card_input if it can avoid it, since outside of a card, e.g. in add_controls!, nothing ignores them. A method should also keep what the user is typing: the method for a Textbox does not change a focused box, unless it is called with force = true, which the card does after an input was applied.
BeamletOpticsGUI.add_controls! — Function
add_controls!(f, gui, title::AbstractString) -> GridLayoutAdds own widgets to the live_view window gui, placed by its layout: with layout = :compact as an entry title of the tool rail (the button "⋯"), which opens the widgets in a popover next to the rail; with layout = :app as a section title of the left sidebar, below "Parameters". f(layout) builds the widgets (e.g. Button, Toggle, Menu, Textbox, or an own widget type, see card_input) into the given GridLayout and connects them. Returns the layout.
Controls are meant for parameters without an object in the scene, e.g. a setting of the whole setup; the widgets of an object belong on its card, see card_rows.
Textboxes and Menus in the layout take the keyboard like those of the live view: while a box is focused or a menu is open, the keys of the 3D view and the camera are ignored. They are collected once f returns, widgets added later are not. An input that changes the optics solves the systems again via retrace!, which makes the change after a running solve is cancelled.
Like the builder of add_panel!, f may return a function update(gui), which is called once right away and after each full solve (not after the preview solves while moving), e.g. to show the current value of a parameter after an undo; update must not change objects. Any other return value of f, e.g. the last listener of a do block, means no update. Errors in update are logged once and do not interrupt the interaction.
add_controls!(gui, "Laser") do layout
box = Textbox(layout[1, 1]; placeholder = "power [mW]", width = 100)
on(box.stored_string) do s
retrace!(() -> set_power!(laser, 1e-3 * parse(Float64, s)), gui)
end
return gui -> card_show!(box, 1e3 * power(laser))
endBeamletOpticsGUI.add_tool! — Function
add_tool!(f, gui, name::AbstractString; icon = :object, key = nothing, toggle = false, tooltip = name)Adds a tool to the live_view window gui: a button that calls f(gui), or with toggle = true a toggle that calls f(gui, active::Bool) whenever it is switched. With layout = :app, an icon button (or toggle) at the end of the toolbar, with the icon and the tooltip; with layout = :compact, an entry (icon and name) of the tool rail opened by the button "⋯". A tool with a key is listed in the help card under "Own tools".
icon is a name of the icon set of the app layout, e.g. :measure, :export or :chart (an unknown name throws an ArgumentError that lists the valid ones, in any layout), or an own Makie.BezierPath, drawn like a scatter marker: in the unit square centered at the origin, e.g. BezierPath("M -0.3 -0.3 L 0.3 -0.3 L 0 0.3 Z") for a triangle.
key, a free Makie.Keyboard.Button such as Keyboard._2 or Keyboard.f5, presses the button (or switches the toggle) from the 3D view; it is shown in the tooltip or label. Keys taken by the live view, the kinematic controls, Makie's Camera3D (which binds almost all letters) or another tool throw an ArgumentError that names the existing binding. The key is ignored while a textbox of the window is focused. Errors in f are logged once. Returns the widget, with clicks (button) or active (toggle).