Releases: nacular/doodle
0.10.1
Features
New SweepGradientPaint
The new sweep gradient paint lets you fill regions with gradients that follow a line that sweeps around a center point. This new paint is a GradientPaint
, which means it allows you to specify a series of colors and stops to create very flexible fills.
canvas.rect(rect, fill = SweepGradientPaint(Red opacity 0.5f, Blue, centerPoint))
canvas.rect(
rectangle = rect,
fill = SweepGradientPaint(
colors = listOf(
Stop(Red, 0.0f),
Stop(Orange, 0.5f),
Stop(Blue, 1.0f),
),
center = somePoint,
rotation = rotation
)
)
APIs
- General
- New
ScrollPanel.scrollChanged
event - New pointer filter events for
Table
header, body and footer available toTableBehavior
s. - New helpers to make creating
MetaRowPositioner
,HeaderCellGenerator
, andFooterCellGenerator
for tables. StyledText
helpers forColor
andPaint
can now target textForeground
.- New
WhenManuallySet
RequiredIndicatorStyle
that lets you set its visibility manually. Resizer
now lets you control which phase of the event process it uses to trigger. This will make it possible for it to word even for Views with children that consume pointer events.- New
lerp
helper forMeasure
s. - New constructor for
BasicCircularProgressIndicatorBehavior
that takes lambdas for paint/stroke. StarRater
now allows rounding of its points- New
star
method variant that takes an innerCircle radius ratio
- New
Fixes | Improvements
-
General
- Built-in Form controls will now scroll into view when focused
View.styleChanged
will now fire when a View is added to a parent that causes itsfont
to change.- Focus traversal for popups and some other cases.
- Added API docs to CommonButtonBehavior and CommonTextButtonBehavior
- Removed duplicate model listener in Table family of classes.
- Slight improvement in change event processing within
FilteredList
. - Modals no longer throw an error if their completion is invoked multiple times.
- Issue where
ModalManagerImpl
could close the wrong modal on completion. - Fixed issue where
DynamicList
did not always remove all the right items when its model changes. Size
no longer throwsIllegalArgumentException
when given negative values forwidth
orheight
. It just maps these to0
instead.- Fixed issue with layer ordering when
DisplayImpl
has a complexfill
- Fixed bug in StyledText.isBlank
- Bug where
ringSection
startCap
andendCap
were flipped StarRater
no longer clips stars when they are rotated- Minor bug in
Resizer
related to cursor updating on pointer up - Edge cases where View's not repainted or laid out when becoming visible after having bounds change while invisible.
-
Browser
- Focus was not properly being returned to the last focused View when focus left/returned to the window.
- Native TextField behavior now avoids prematurely clearing focus, so it can be regained when the window is selected again.
- Native HyperLink behavior now clicks the underlying link in response to the
fired
event. - Fixed
View
clipping withinDocumentImpl
- Issue with SVG shadows in some cases.
- Fixed issue where shadow filter not cleared correctly on reused element
- Fixed text baseline alignment issue when using svg
- Addressed some inconsistencies in how text is rendered via SVG vs plain HTML.
- Issue where outer shadow could have incorrect offsets
- Outer shadows could be clipped when used withing
PatternPaint
.
-
Desktop
- Fixed issue with text shadows. Inner shadows are now supported.
- Fixed issue where native views in popups were not assigned to any Display and therefore Swing peers were not added to the scene. This broke events for these Views.
Versions
- Mockk -> 1.13.9
- Dokka -> 1.9.20
0.10.0
Features
Hosting arbitrary HTML elements
You can now embed any HTML element into your app as a View. This means Doodle apps can now host React and other web components and interoperate with a much larger part of the Web ecosystem out of the box!
import io.nacular.doodle.HtmlElementViewFactory
import io.nacular.doodle.application.Application
import io.nacular.doodle.core.Display
import org.w3c.dom.HTMLElement
class MyApp(
display : Display,
htmlElementView: HtmlElementViewFactory,
someElement : HTMLElement,
): Application {
init {
display += htmlElementView(element = someElement)
}
override fun shutdown() {}
}
application(modules = listOf(Modules.HtmlElementViewModule)) {
MyApp(display = instance(), viewFactory = instance(), element = element)
}
WASM JS Support
Doodle now supports the wasmJS
build target. This means apps can also target WebAssymbly for the browser. The APIs/features for this new target are identical as those for the js
target; which means code can be shared between apps targeting both. The only difference is that the application
launchers need to be called from separate source sets (i.e. jsMain
vs wasmJsMain
).
Multi-window Support (Desktop)
Apps for Desktop can now create/manage multiple windows using a new WindowGroup
instance. This instance can be injected into an app just like the Display
. It then provides APIs for getting the main
window and creating new ones. Single window apps continue to work as they did before. That is, an app that injects the Display
will receive the main
window display and can manipulate it as before. But apps that want to manage their window(s) will need to inject this new type.
class MyCoolApp(windows: WindowGroup /*, mainWindowDisplay: Display*/): Application {
init {
// main window's display, same as if injected
windows.main.apply {
title = "Main Window"
// manipulate main window's display
display += view {}
}
// create a new window
windows {
title = "A New Window!"
size = Size(500)
enabled = false
resizable = false
triesToAlwaysBeOnTop = true
// manipulate the new window's display
display += view {}
display.layout = constrain(display.first(), fill)
closed += {
// handle window close
}
}
}
override fun shutdown() {}
}
Native Window Menus (Desktop)
Apps can now set up native menus for their windows. This looks a lot like working with the existing menu APIs, but it results in changes to the OS window decoration. These menus are just as interactive as the in-app ones as well, meaning they trigger events when the user interacts with them.
window.menuBar {
menu("Menu 1") {
action("Do action 2", pathIcon) { /*..*/ }
menu("Sub menu") {
action("Do action sub", icon = simpleIcon) { /*..*/ }
separator()
prompt("Some Prompt sub") { /*..*/ }
}
separator()
prompt("Some Prompt") { /*..*/ }
}
menu("Menu 2") {
// ...
}
}
Native Window Context Menus (Desktop)
Apps can now set up native context/popup menus for their windows. The API is very similar to native menus.
window.popupMenu(at = somePoint) {
action("Do action 2", pathIcon) { /*..*/ }
menu("Sub menu") {
action("Do action sub", icon = simpleIcon) { /*..*/ }
separator()
prompt("Some Prompt sub") { /*..*/ }
}
separator()
prompt("Some Prompt") { /*..*/ }
}
Key events behave more like Pointer events
Key events now "sink" and "bubble" like pointer events. This means ancestor Views can intercept (and veto) them before they are delivered to their target (the focused View). They also bubble up to ancestors after being delivered to the target if they are not consumed. The notifications for the first phase happen via a new View.keyFilter
property, while the bubbling phase is notified via the existing View.keyChanged
property.
APIs
-
General
- View's can now pass pointer events through to underlying views. This is managed by the value returned by
View.shouldHandlePointerEvent
andView.shouldHandlePointerMotionEvent
. - It is also possible to be notified of pointer events that are passed through a View using the new
View.pointerPassedThrough
andView.pointerMotionPassedThrough
properties. - Modals can now specify how their backgrounds affect other modals that they overlay. This is configured via
ModalContext.backgroundMode
. - Modals can now have pointer events pass through using the new
allowPointerThrough
property. Setting it to true will allow all pointer events to pass through the background; while still notifyingpointerOutsideModalChanged
andpointerMotionOutsideModalChanged
. - New
View.toParent
method for taking a point from a View's coordinate space to its parent's PathBuilder
now supportsmoveTo
Resizer
can now avoid updating a View's cursor ifmanageCursor = false
is provided to it at construct time.VerticalList
andHorizontalList
builders now take optionalitemVisualizer
View.ClipPath.path
can now be overridden by subclasses.SetPool
no longer exposes a constructor, or underlying data structure, so it can be controlled.- New
PathModule
to allow use ofPathMetrics
.PathMetricsImpl
is now internal. - New
MenuFactoryModule
to allow use ofMenuFactory
.MenuFactoryImpl
is now internal. - New
UserPreferencesModule
to allow use ofUserPreferences
.UserPreferencesImpl
is now internal. - New
Rectangle.centered(at)
utility method. - New
PatternPaint.opacity
property. Paint
constructor now internal.- Positions in constraints can now be added/subtracted with
Point
. Resizer
can now havemovable
property set via constructor.- New
Ellipse.inset
method for insetting an ellipse - Moved
Circle.inscribed
up toEllipse
so polygons can be embedded within ellipses as well. NumericAnimationPlan.invoke
parameter renamed toonChange
for consistency/clarity.- Make
Modules
constructors private. - Deprecations
- Old io.nacular.doodle.controls.menu package removed
Label.horizontalAlignment
removedTextVisualizer
typealias
removedCanvas.wrapped
methods removedinscribed
method removed fromPolygon.kt
PointerInputService.Listener.changed
method removedPointerInputService.Preprocessor.preprocess
method removed
- View's can now pass pointer events through to underlying views. This is managed by the value returned by
-
Browser
- Remove drag-drop support on IE
- Moved all DOM definitions to internal, so they don't pollute the app space.
- Maked
NativeFocusManager
@internal andsealed
-
Desktop
createApplication
should've been@Internal
.
Fixes | Improvements
-
General
- Issue where Carousel item not properly updated if
skip
called when there is notransitioner
- Issue where work done during
View.addedToDisplay
could cause unbroken loop. ModalManager
now clears focus when a new modal is shown and returns focus to the previous focus owner when that modal is completed.- Issue in
FocusManagerImpl
related to transitioning focus to a new View - Issue in
RenderManagerImpl
where new Views could be rendered during an ongoing render. This would result in concurrent modification errors since the latest Kotlin version adds guards for this in JS InternalMap. - Edge case where
View
's transform could be out-of-sync for right-left Views that need mirroring. - Improved
Resizer
drag for some cases when the targetView
has a transform. - Issue where zero sized View might get stuck in render pipeline.
- Issue with incorrect time provided by
AnimatedScheduler.onNextFrame
. - Support for
ImagePaint
- Issue where Carousel item not properly updated if
-
Browser
- Fixed pointer handling of
ENTER
, which is called on scroll and new Views are added. This means pointer events are now delivered properly in these cases. - Suppress scroll in native
TextField
behavior when element focused - Issues with canvas clipping and transforms within custom
FileSelector
behavior - Handle selection attempt for input types that do not support it
- Fix issue where native
FileSelector
would not triggerfilesChanged
if the same item was selected in subsequent viewings. - Concurrent modification issue in
SetPool
addressed w/ copy-on-write semantics - No longer having DOM types resolved via instance() in certain Kodein bindings since that results in WASM compiler crashes
- Avoiding concurrent modification issue with
RenderManagerImpl.pendingBoundsChange
- Fixed issue where Chrome would raise invalid key events (null properties) when doing autofill for form fields.
- Fixed Pointer handling for apps nested Views from
ApplicationViewFactory
. - Issue where browser could get into loop with Mutation Observer events.
- Issue with shadow clipping for SVG rendering.
- Arc and Wedge not properly outlining.
- Fixed pointer handling of
-
Desktop
- Fixed crash on app shutdown
Build
- Fixed JS Source Maps
- Using toml for buildSrc kotlin version
- Add expect-actual-classes
- Minor improvements to buildSrc code
- Adding WasmJs to signing dependency chain
Versions
- Kotlin -> 1.9.22
- Skiko ...
0.9.3
Features
New Carousel Control
The new Carousel
control is a View
that contains a lazy list of items generated from a ListModel
. It delegates all decisions about what items to show to a Presenter
obtained from its CarouselBehavior
. The Carousel
only displays items the Presenter
requests as it moves through frames. This means it can scale to very large datasets as long as the Presenter
only shows items that are visible.
Carousel
s can have a very wide range of layouts and behaviors. This flexibility is achieved by giving Presenter
s full control over which items (including supplemental ones not based on the data model) are shown at any point, how opaque they are, their positioning, size, transform, clipping, etc.. With this, you can create almost any kind of carousel experience you'd like.
Carousel
transitioning is a key part of their behavior. The control is therefore fully dynamic and interactive. You can move between frames in two ways. Either by skipping ahead or backward or by manually moving the Carousel to an x/y offset and then letting it transition to the best frame when manual movement is complete. Carousel
s rely on a Transitioner
to manage the way they animate for all these movements. This allows a great deal of flexibility and customization.
Presenter
s are responsible for updating frame state whenever the Carousel
adjusts anything. This allows for holistic animations. There are several built-in Presenters
that show how to create various effects as well.
val carousel = Carousel(
SimpleListModel(listOf(image1, image2, image3)),
itemVisualizer { item, previous, _ ->
when (previous) {
is DynamicImage -> previous.also { it.update(item) }
else -> DynamicImage(item)
}
}
).apply {
wrapAtEnds = true
acceptsThemes = false
behavior = object: CarouselBehavior<Image> {
override val presenter = LinearPresenter<Image>(spacing = 10.0) {
val aspectRatio = it.width.readOnly / it.height.readOnly
it.width eq parent.width
it.center eq parent.center
it.height eq it.width / aspectRatio
}
override val transitioner = dampedTransitioner<Image>(timer, animationScheduler) { _,_,_, update ->
animate(0f to 1f, using = tweenFloat(easeInOutCubic, duration = 1 * seconds)) {
update(it)
}
}
}
}
Animations Can Now Be Paused/Resumed
The Animation
s implement the new Pausable
interface, which allows them to be paused and resumed at any time.
val animation = animate.invoke(0f to 1f, using = tweenFloat(easeInOutCubic, duration = 1 * seconds)) {
// ...
}
animation.pause()
// ..
animation.resume()
view
| container
Improvements
These DSLs now expose more of the protected properties of View
and Container
respectively.
view {
+ view {} // adds a child to the View
chidren += view {} // children now accessible
layout = simpleLayout {} // access layout
addedToDisplay = { } // called when view added to display
removedFromDisplay = { } // called when view removed from display
contains = { _ -> false } // called to decide if a point within the view
// ..
}
APIs
-
General
Vector3D
now exposes itsmagnitude
basicMenuBehavior
module function now exposes various configuration input parameters to customize the result.TreeItemRole
now has aselected
property- New lerp function for
Rectangle
andSize
- New
Rectangle
constructor that takes aSize
BasicDropdownBehavior
now takes Accessibility label for its buttonBasicMutableDropdownBehavior
now takes Accessibility label for its buttonBasicSpinnerBehavior
now takes Accessibility labels for its buttonsBasicMutableSpinnerBehavior
now takes Accessibility labels for its buttonsbasicDropdownBehavior()
,basicMutableDropdownBehavior()
,basicSpinnerBehavior()
andbasicMutableSpinnerBehavior()
now support accessibility labelsBasicDropdownBehavior
andBasicMutableDropdownBehavior
now allowinset
to be specifiedbasicDropdownBehavior()
andbasicMutableDropdownBehavior()
now take an optionalinset
MonthPanel
class is now open- New
FieldState.ifInvalid
utility function
-
Browser
- Auto-complete can now be disabled for
nativeTextFieldBehavior
- Auto-complete can now be disabled for
Fixes | Improvements
-
General
- Bug where
MonthPanel
could NPE if selection queried while nothing selected - Bug in
TextMetrics
size calculation whenlineSpacing
set TreeRow
now updates its accessibility role with the current selected state.- Issue where deleted Views might not be cleaned up if they are removed from a parent at the same time their child is being removed and added to that same parent.
- Bug where
View
could loop when a child is removed and added to its parent - Camera now clips transformed ConvexedPolygons so their points do not go behind it.
- Issue where the children of a
View
that is removed are not properly re-added if they are placed into their grandparent before the next cleanup pass withinRenderManagerImpl
. - Bug where
View.toAbsolute
andView.fromAbsolute
resulted in incorrect results - Optimize
View.resolvedTransform
with caching - Issue where
TextField
could not override user input duringtextChanged
nativeTextFieldBehavior
respectsTextField.cursorVisible
TextInput
settingcursorVisible
totrue
during selection- Slight UI improvement for inset in basic
BasicDropdownBehavior
- Issue where
textField
form element would be considered invalid at creation when blank text is allowed StyledText.isBlank
not working as expectedMonthPanel
more efficiently responds to selection changes, especially when the selection model has a very large dataset.
- Bug where
-
Browser
TreeRole
now marked as aria-multiselectable- Native behavior for
HyperLink
s ensure the associated html element inherits the a11y labels of their Hyperlink - Native behavior for
TextField
s ensure the associated html element inherits the a11y labels of their TextField - Fixed issue where Native
TextField
behavior stopped supporting mask value - Fixed Issue with graphics surface sort order
- Line-height not correctly set when value != 1f
- Bug where zOrder is incorrect for containers
- Only apply Safari shadow hack when multiple shadows applied to an element
- Case where graphics surface index could go negative and create incorrect render ordering
nativeTextFieldBehavior
now properly respectsTextField.cursor
- Improved text fitting for
nativeTextFieldBehavior
-
Desktop
- Bug with zIndex for pop-ups
Documentation
- Improving code documentation
- proper external links
- Initial module docs
Versions
- Measured -> 0.3.3
- Dokka -> 1.9.0
0.9.2
Features
New Menu Control
The new Menu
control is a View
that contains a vertical list of interactive items. These items notify whenever the user interacts with them using a pointer or the keyboard. This control support a couple types of action items and one that shows a new "sub" menu when the user interacts with it. You specify the contents of a Menu
using the MenuFactory
, which provides a declarative DSL for defining the structure and behavior of Menus.
val menu = menus(close = { /* close menu */ }) {
menu("Sub menu") {
prompt ("Prompt" ) { /* handle */ }
separator( )
action ("Do something", icon) { /* handle */ }
menu ("Sub-sub menu" ) {
action ("Some action" ) { /* handle */ }
separator( )
action ("Another action") { /* handle */ }
}
}
action("Execute action") { /* handle */ }
}
TextFields now expose a purpose
property
This new property provides a way to specify the use-case for a TextField, which allows its behavior to change rendering or what keyboard to show on mobile.
val textField = TextField().apply {
purpose = Email
}
Unmanaged Scroll on Browser
NativeScrollPanelBehavior
(for Browser) now takes a parameter that indicates whether it should use managedScrolling
. Managed scrolling is what Doodle has done for a while now. It lets the framework directly handle positioning the ScrollPanel
contents as the user scrolls. This is very powerful and flexible; but it relies on the browser invoking the onscroll
event at a high framerate to avoid lag. Unfortunately this isn't always the case. Mobile WebKit does not deliver these events fast enough to make it smooth. So developer can choose to disable this behavior via the new flag.
APIs
- Deprecating previous types in
io.nacular.doodle.controls.menu
Icon
's generic bounds have been expanded toAny
. This makes it easier to create icons that are based on arbitrary data instead of onlyView
s.PathIcon
can now be created with aStroke
andPaint
, making it more customizable.- Helper for creating inset
Rectangle
Fixes | Improvements
- General
- Fixing many build warnings
- Right-left issue for popups and
BasicDropdownBehavior
- Enabled parallel builds
- Edge case in View to/from absolute
BasicDropdownBehavior
andBasicSpinnerBehavior
now use rounded joints/end-caps for their arrow paths- Issue where
Theme
not applied to existing popups whenThemeManager.selected
called - Expanded
ThemeManagerImpl
tests - New tests for
RealGraphicsSurface
View index and zOrder - New tests for
PopupManagerImpl
- New tests for
HorizontalFlowLayout
- Build
- Adopted Gradle library bundles using toml file
- Browser
- Context menu is now suppressed if the pointer pressed event is consumed. #52
- Incorrect default font-weight in global styles
- Display no longer sets a background color when filled w/
Transparent
zOrder
now behaves as it should: only affecting ordering within a container. It also no longer allows Views to render above popups. #53zOrder
no longer affects how popups are displayed- Issue where multiple shadows not rendered properly by Webkit
Versions
- Mockk -> 1.13.5
- Gradle -> 8.1.1
- LogBack -> 1.4.7
- slf4j-api -> 2.0.4
0.9.1
Dokka APIs Available
API docs for Doodle are now available at https://nacular.github.io/doodle-api/
Features
New PopupManager
You can now present top-level Views that will cover all other existing ones in a safe and reliable way using the new PopupManager
. Before, you could make custom popups by directly adding items to the Display
. This worked in many cases, but had a lot of limitations. For example, Views added this way would be affected by any Layout
on the Display
, which meant you couldn't control these Popups as easily. Also, new Views added to the Display
after a custom popup was shown could overlay the popup. So there was no way to guarantee that a popup remained the top-most View. The new PopupManager fixes these limitations and provides a simple API for showing popups.
popupManager.show(view) {
// size / position popup
it.height eq parent.height / 2
it.width eq it.height * 1.5
it.center eq parent.center
}
Popups can be positioned relatvie to another View as well by doing the following.
popups.show(view, relativeTo = someView) { popup, someViewRect ->
// size / position popup
(popup.top eq someViewRect.y ) .. Strong
(popup.left eq someViewRect.right + 10) .. Strong
(popup.bottom lessEq parent.bottom - 5) .. Strong
popup.top greaterEq 5
popup.left greaterEq 5
popup.right lessEq parent.right - 5
popup.height eq parent.height / 2
popup.width eq popup.height * 1.5
}
Any View Can Be A Popup
The PopupManager's API works with View
, so you can make anything a popup. Simply do the following:
popupManager.show(myView) {
it.center eq parent.center
}
// ...
popupManager.hide(myView)
Views that are shown as popups will automatically become top-level (being removed from their existing parent if already displayed) and sit above all other existing Views. Hiding a popup removes it from the Display
, but it won't return the View to a previous parent if it had one.
Popup Layouts
Popups are shown with layout information directly. There are two ways to show a popup: relative to the Display
, or relative to another View
and the Display
. There is an API call for each:
Here, myView
is positioned with only a "reference" to the Display (parent).
popupManager.show(myView) {
it.center eq parent.center
}
But sometimes a popup needs to be positioned relative to another View. This shows how to place myView
so it tracks the bounds of someView
. The PopupManager will handle keeping the popup aligned with someView
.
// myView is positioned with only a "reference" to the Display (parent)
popupManager.show(myView, relativeTo = someView) { popup, anchor ->
popup.top greaterEq 0 // Popup top-left always visible
popup.left greaterEq 0 // Popup top-left always visible
popup.width.preserve // don't shrink popup
popup.height.preserve // don't shrink popup
(popup.right lessEq parent.right ) .. Strong // stay in parent as long as doesn't compress popup
(popup.bottom lessEq parent.bottom) .. Strong // stay in parent as long as doesn't compress popup
(popup.top eq anchor.bottom + 10) .. Medium // follow anchor, as long as stronger constraints not in conflict
(popup.centerY eq anchor.centerY ) .. Medium // follow anchor, as long as stronger constraints not in conflict
}
New ModalManager
Modals are now easy to incorporate into your apps with the help of the new ModalManager
. This component uses the PopupManager
to display Views that behave like modals (they require user input to be dismissed). Modals are strongly typed and return a single value upon completion. They also have a customizable overlay that will obscure the underlying Views if painted.
// launch modal and await result (suspending)
val value: T = modalManager {
Modal(
// View used as modal
view {
// ...
// call completed when the modal is done
completed(result)
}
) {
// optionally provide a layout block
// or the view will default to being
// displayed in the center
}
}
Modals can also be positioned relative to another View.
// launch modal and await result (suspending)
val value: T = modalManager {
RelativeModal(
// View used as modal
view {
// ...
// call completed when the modal is done
completed(result)
},
relativeTo = someView
) { modal, someViewBounds ->
// position relative to parent and someView
}
}
More Powerful Text Rendering
Text Alignment
There is a new TextAlignment
enum that controls how wrapped text is displayed within its margins. This is a replacement for HorizontalAlignment
, which currently uses Left
, Right
(instead of Start
, End
) and does not support Justify
. You can justify text by doing the following:
canvas.wrapped(
text,
at = Origin,
width = width,
alignment = TextAlignment.Justify,
fill = Black.paint
)
canvas.wrapped(
styledText,
at = Origin,
width = width,
alignment = TextAlignment.Justify,
)
Letter, Word, And Line Spacing
You can now control the way letters, words and lines are spaced when rendering text (via Canvas.text(...)
and Canvas.wrapped(...)
). Letter and words spacing can be provided to the text rendering methods on Canvas
using the new TextSpacing
class. This information can also be passed to TextMetrics
when measuring text. Label
also has support for both letter and word spacing.
Line spacing can also be specified whenever you deal with wrapped text.
New LazyPhoto Widget
This new component (in controls
lib) takes a Deferred<Image>
and renders it when loading is complete. It also takes a custom renderer that it uses to draw itself while loading. This allows customization and even animation.
APIs
Rectangle.toPath
that allows specifying each corner radius- Exposing
insets
insimpleTextButtonRenderer
function repeat
andloop
animations can now have delaysCanvas
can now render wrapped text with custom line-spacingLabel
now haslineSpacing
property which controls how it displays wrapped text- Animatable Properties are no longer restricted to use within Views
- New method to create
Ellipse
andCircle
by inscribing within Rectangle - New
Circle.diameter
property - New constructor for StyledText that takes a
String
andStyle
- Updated parameters in
Canvas.wrapped
methods to make clearer (i.e.indent
/width
vsleftMargin
/rightMargin
) - Added new
StyledTextVisualizer
to mapStyledText
->Label
- Deprecated
TextVisualizer
and introducedStringVisualizer
defaultLayout
inNamedConfig
(labeled form control) now takes an optional itemHeight- New
tableCellEditor
DSL
Accessibility
- Browser
Button
now setsaccessibilityLabel
to itstext
if no label is already providedHyperLink
with native behavior will now apply aria-label to the anchor tag using the HyperLink's text when theAccessibilityManager
is present
Fixes | Improvements
- General
- Bug with empty AnimationBlock when created during active animation
- Wrapped text not correctly aligned
- Bug in FilteredList
iterator.remove
- CommonLabelBehavior no longer provides an x offset for wrapped text since the alignment is handled correctly by Canvas
- Edge case where layout can hang b/c of double imprecision
- Issue where
CommonLabelBehavior
overridesLabel.foregroundColor
on uninstall - Bug where incorrect item could be removed from
RenderManagerImpl.pendingLayout
- Bug where some View properties lost during behavior install
- Bug where some views not cleaned up by RenderManager
- Bug in pointer handling when Display transformed
- ToggleButton no longer relies on being displayed to listen to its model
- StyledText.text now returns correct value
behavior
delegate now re-renders theView
when a newbehavior
is installed. This removes the need for Behaviors to callrender
explicitly upon install- Bug where
DynamicList
would have incorrect selection when selection set before items added
- Browser
- Misidentifying elements as native scroll panels led to incorrect pointer behavior
- Reuse instances for linear/radial gradient paints
- Reusing clipped images
- Hyperlinks now open in new tab
- Shadow render bug when using SVG
- Element sometimes not reused when drawing shadow
- Remove overflow on element w/ shadow
- Fixed wrapped StyledText rendering
- Wrapped StyledText now supports text decoration on previously unsupported cases
- Wrapped text no longer indents if the indent would result in the first word overflowing the width
- Issue where reused text element could retain wrapped styles
- Issue where text background color isn't properly cleared
- Cleaning up text-alignment for reused
<b>
elements - Issue where styled text width not properly cleared
- Desktop
- Fixed SVG image file loading issue
- Fixed rendering of images with radius
- Issue where font families weren'...
0.9.0
Features
New Constraint Layout Fully Adopted
The new layout system was introduced in 0.8.2 alongside the former approach is now fully adopted and the old system has been removed. You can get more details on how the new system works in the previous release notes. This release does bring a few additional changes as well. The main difference is that parent constraints are now "constant" by default. This means constraints like the following will NOT modify the parent bounds:
constraint(view) {
it.width eq parent.width - 10 // parent.width is treated like a simple number and is not updated to meet the constraints
}
However, you can still make parent properties writable by being explicit as follows:
constraint(view) {
it.width eq parent.width.writable - 10 // now parent.width could be modified if needed to meet the constraints
}
There were also some improvements to this new layout engine. Constraints now default all parent properties to read-only. This means it is no longer possible to use the readOnly
field on parent properties. Instead, each can be turned into a "writable" value using the new writable
field on each.
Revamped Animation APIs
The Animation APIs have been updated to make them more powerful and easier to use. You can now animate a lot more data types, with built-in support for common ones like (Int, Float, Double, Size, Position, Rectangle, Color, and Measure). It is also easy to add new types when they can be converted to a numeric representation. The animation APIs also let you create animatable properties, and provides a few different types of animations, including tweens, key-frames, and repetition.
New Diff API for ObservableList
A new list diff algorithm (based on Google's diff-match-patch) was introduced internally in 0.8.2 to support the new constraint system. It has now been adopted as the mechanism for notifying about changes to ObservableList
and related types (i.e. FilteredList
). This algorithm scales better and more intuitive than what was in place before. But the APIs are very different.
Old
observableList.changed += { list, removed: Map<Int, T>, added: Map<Int, T>, moved: Map<Int, Pair<Int, T>> ->
}
New
observableList.changed += { list, changes: Differences<T> ->
}
The new API indicates changes via the Differences
interface. This interface lets you iterate over a series of Difference<T>
instances that indicate what change was applied to the list at a given index.
observableList.changed += { list, changes: Differences<T> ->
changes.forEach { difference ->
when (difference) {
is Delete -> {}
is Insert -> {}
else -> {} // Equal
}
}
}
Differences
also let you compute moves optionally in case they matter to your handling of changes.
diffs.computeMoves().forEach {
when (it) {
is Insert -> {
it.items.forEach { item ->
if (it.origin(of = item) == null) {
// not a move
}
}
}
is Delete -> {
it.items.forEach { item ->
when (val destination = it.destination(of = item)) {
null -> {
// not a move
}
else -> {
// move
}
}
}
}
else -> {}
}
}
Improved Tables and TreeTables
- Table/TreeTable now support footers
- Table/TreeTable now allow changing the visibility of their header (and footer)
- Tables and their related types now handle being added to a
ScrollPanel
. In this case they will BasicMutableTableBehavior
now shows sort order via an icon- New
EqualSizePolicy
andProportionalSizePolicy
types to support different table header sizing strategies - New way to specify the stickiness of headers and footers within tables and their derivatives
- New
KeyValueTable
to simplify showing Maps in tabular form - Table and its derivatives no longer prevent their first column from resizing
BasicTreeTableBehavior
now takes aniconFactory
instead of an icon colorTreeTable
now scales the contents of its internal ScrollPanel likeTable
doesTableHeaderCell
(used for basic table behaviors) now repaints when disabled/enabled to update colors correctly.TableHeaderCell
no longer changes color on pointer pressed if its column is not movableTableHeaderCell
no longer uses theGrabbing
icon when draggingBasicMutableTableBehavior
now reflects initial sorting of its table.- Ideal size for Table (and derivatives)
- TableBehavior moveColumn now provides the distance it will move. This allows for constant velocity animations
New FileSelector Control
Form Controls
- New
slider
,rangeSlider
,circularSlider
andcircularRangeSlider
controls - New
file
andfiles
controls
APIs
- Replaced
constrain(videw: View, within: Rectangle)
method with faster<T: Positionable> Iterable<T>.constrain(using: ConstraintDslContext.(Bounds) -> Unit, within: (Int, T) -> Rectangle)
- new
ifNull
utility function - ScrollPanel now exposes scrollbar dimensions and notifies listeners when they change.
- CheckBoxRadioButtonBehavior now takes an
iconInset
which indicates the padding around the icon (does not apply to icon-text spacing gap). - BasicMutableTableBehavior now takes a footerColor
- New
simpleTableCellEditor
function for creatingTableEditor
s that modify a single cell at a time. - New
ConvexPolygon.map
function to create a new polygon by transforming another - New DSL to use an ItemVisualizer of a different time after mapping its inputs:
ItemVisualizer<R, C>.after(mapper: (T) -> R): ItemVisualizer<T, C>
- New
ExpandableItem
interface for use with Tree and related views. - [API] MutableTreeNode's children is now a MutableList
- Updated
path(data: String)
function, so it can returnnull
- New
cancelable
delegate
Performance
- General
- Minor change to AffineTransformImpl to support single Point invocations without converting to array/list
- RenderManagerImpl now uses View.generationNumber instead of an ancestor comparison for layout order
- ConstraintLayoutImpl handles changes in constant parts of constraints more efficiently. This makes
readOnly
usage faster.
- Browser
- Now using native JS Set/Map in some performance critical areas
- TreeSetJs no longer uses recursion
Fixes | Improvements
- General
- Bug in
HorizontalFlowLayout
when container is empty - typo in ScrollPanelConstraintDslContext property
optionalRadioList
now allows deselection of its items- Layout of some Form list items
- SplitPanel constraints no longer modify parent
- ScrollPanel how ignores layout when its content scrolls
- ListItem no longer replaces layout if children remain the same
- Bug in
Table
that could lead to an incorrect index being used when generating a row - Bug in
TreeTable
that could lead to an incorrect index being used when generating a row - Bug in basic list positioning for cases when the list has now width or height
- Bug in
FilteredList
when filter isnull
- Improved how Table and TreeTable handle visible scrollbars. They both now avoid showing horizontal bars and adjust their headers accordingly if the vertical bar is visible.
- Bug in TreeColumns where items weren't being recycled
- Bug in View.generationNumber updating
- Issue in CheckBoxRadioButtonBehavior where icon inset was not being included in the button's idealSize.
MutableTable
keeps itself sorted when its model changes.- Bug in ListItem that caused default
cellAlignment
to be ignored - No longer doing layout in
TreeTable
whencolumnSizePolicy
changed before it has abehavior
View.scrollTo
now handles nestedScrollPanel
s- List now checks the point that is 1 pixel up and left from its display rect bottom-right corner when updating visible cells. This avoids accidentally including cells that are hidden.
- Bug in Tree that rendered rows incorrectly in some cases
- ScrollPanel now matches content ideal size right away
- Bug in
- Browser
- Minor icon alignment issue in native button behavior
- Native button behavior now properly updates button
idealSize
- Bug in native Slider snapping Behavior for some values of ticks
- Default Key event behavior now prevented when event consumed.
- Desktop
- Bug in desktop DragManagerImpl where unsupported MimeTypes were not properly handled
Dependencies
- Kotlin -> 1.7.21
- Kodein -> 7.16.0
- Skiko -> 0.7.40
- Kover -> 0.6.1
- Dokka -> 1.7.20
- Measured -> 0.3.2
0.8.2
Features
New Cassowary based Constraint Layout
This release includes a new layout engine that will soon replace the current constraint based layout. This new implementation is based on the well known Cassowary algorithm and is therefore much more expressive and capable. The new implementation has a very similar API to the current one, so migration is less difficult. But there are key differences that mean current constraints won't always translate exactly. The new API is located in the io.nacular.doodle.layout.constraints
package.
Basic Usage
Constraints are created using the new constrain
builder found in io.nacular.doodle.layout.constraints
. It takes a list of Views and a constraint block with rules for how those Views are related to each other and their parent. Each View is mapped to a Bounds
within the constraint block. These contain properties that let you manipulate the View's bounds
.
constrain(view1, view2, view3) { v1, v2, v3 ->
// constraints defined here
}
Constraints are now specified using equations and inequalities. These are written as expressions that contain a single operator to indicate equal (eq
), less-than-or-equal (lessEq
), and greater-than-or-equal (greaterEq
). The layout will solve the set of equations and assign values for each of the properties within each relationship.
constrain(view1, view2, view3) { v1, v2, v3 ->
v1.right eq parent.right - 10
v2.right lessEq v1.left - 10
v1.height + v2.height + v3.height eq parent.height
}
Expressions with eq
are similar to what the legacy constraint system offers. But there are still very important differences (see below). While lessEq
and greaterEq
are entirely new ways of expressing relationships. They provide a much easier way to express boundary conditions than the legacy system.
This example shows how you could keep a view's horizontal bounds confined to a region within its parent while still letting it move or scale.
constrain(view) {
it.left greaterEq 10
it.right lessEq parent.right - 10
}
Differences Compared to Legacy
Constrain Blocks Are Live
The new constraint builder looks a lot like the legacy one: a list of Views with a configuration block. But there is a major difference. The new layout will actually invoke that provided block on every layout, while the legacy implementation only calls its block once, treating it as data rather than an active handler.
This makes the legacy layout a lot more cumbersome and less intuitive. That is because it is not clear at all that the constraint lambda does not behave like others and will only be invoked once. This fact also means legacy constraints require extra hacks to capture external variables that update over time. It is also impossible to use control logic with the legacy system to dynamically change constraints; this is no longer an issue.
Legacy
constrain(view) {
it.width = parent.width - capturedValue // capturedValue is only read once when the layout is constructed
}
constrain(view) {
it.width = parent.width - { capturedValue } // an inner lambda is needed to keep capturedValue updated
}
container.layout = constrain(view1, view2) { v1, v2 ->
// This will only be evaluated once when the Layout is constructed,
// which means the constraint won't flip as container.width changes
when {
container.width < 100 -> it.width = parent.width / 2
else -> it.width = parent.width
}
}
New
constrain(view) {
it.width eq parent.width - capturedValue // capturedValue is read on every layout as expected
}
container.layout = constrain(view1, view2) { v1, v2 ->
// This will result in different constraints being applied dynamically as
// container.width crosses 100
when {
container.width < 100 -> it.width eq parent.width / 2
else -> it.width eq parent.width
}
}
Views Don't Need To Be Siblings
You cannot constrain Views that are not siblings in the legacy implementation. This is intended to avoid unintuitive behavior, but it made constraints more cumbersome to use, since it forces all Views to be in the same container before configuration. The new implementation takes a different approach. It allows you to configure any set of Views, regardless of their hierarchy. But, it only updates the Views that within the Container it is laying out. All other Views are treated as readOnly
. This adjustment happens automatically as the View hierarchy changes. A key consequence is that Views outside the current parent will not conform to the constraints. This avoids the issue of a layout for one container affecting the children of another.
Legacy
val view1 = view {}
val view2 = view {}
val container1 = container{
children += view1
layout = constrain(view1, view2) { v1, v2 ->
v1.width = v2.width // <======================= Error since view2 does not share the same parent ❌
}
}
New
val view1 = view {}
val view2 = view {}
val container1 = container{
children += view1
layout = constrain(view1, view2) { v1, v2 ->
v1.width eq v2.width // <======================= v2.width treated as immutable value (i.e. v2.width.readOnly) ✅
}
}
Relationships Are Bidirectional
Legacy constraints are all defined using assignment, making them inherently unidirectional. In the example below, v1
would get its width from v2
, but v2
's width would not be modified.
Legacy
constrain(view1, view2) { v1, v2 ->
v1.width = v2.width // v2.width would not be modified
// v2.width = v1.width is not the same as above
}
The new implementation produces bidirectional constraints by default. This means expressions can be written like mathematical equations, where the order of terms is often (but not always) unimportant. It is still possible to create unidirectional relationships using readOnly
on a property or an entire expression.
New
constrain(view1, view2) { v1, v2 ->
v1.width eq v2.width // both v1.width and v2.width can be changed to ensure they are equal
// v2.width eq v1.width is the same as above
}
constrain(view1, view2) { v1, v2 ->
v1.width eq v2.width.readOnly // v2.width won't be modified
(parent.height - v2.height).readOnly eq v1.height // neither parent.height nor v2.height will be modified
}
Bidirectionality applies to parent constraints as well (if the View is not top-level). So the following constraint might actually update the parent's width.
constrain(view) {
it.width eq 100
it.width eq parent.width // parent.width would be modified to ensure the equality
}
This can be prevented by using readOnly
.
Constraints Added/Removed Symmetrically
The legacy constraint system allows single Views to be unconstrained. This is helpful when you need to let a View move more freely, or when it is removed from its parent. The API for this is as follows:
Legacy
val layout: ConstraintLayout = constrain(view1, view2) { v1, v2 ->
// ...
}
layout.unconstrain(view2) // all associated constraints removed
The new API changes this approach to avoid confusion now that constraint blocks are invoked repeatedly during layout. This avoids the case where some subset of the constraints in a block are "removed", even though the block is still being invoked. This means constraint blocks behave a lot more like event handlers.
New
val constraints: ConstraintDslContext.(Bounds, Bounds) -> Unit = { v1, v2 ->
// ...
}
val layout: ConstraintLayout = constrain(view1, view2, constraints)
// layout.unconstrain(view2) // no longer available
layout.unconstrain(view1, view2, constraints) // remove all constraints in a block
Constraints Have Strengths
The new API allows you to define relationships that can be in conflict with each other. Such situations result in an error since they have no clear solution. API also provides a way to resolve such cases with a relative priority or strength of constraints. This allows the engine to break lower strength constraints when there are conflicts.
All constraints have the Required
strength by default. This is the highest possible strength that tells the engine to enforce such a constraint. But you can specify the strength explicitly as follows.
constraint(view) {
it.left eq 0
it.width greaterEq 100
(it.right eq parent.right) .. Strong // constraint will be broken if needed since the one above is Required
}
Form Controls
- New
singleChoiceList
for choosing a single item likeradioList
offers - New
optionalSingleChoiceList
for choosing zero or one item likeoptionalRadioList
offers
APIs
Always
andWhenInvalid
RequiredIndicatorStyle
s (used for form fields) no longer require an explicit string when constructed, and will default to using "*"labeled
Form fields can now change the vertical spacing between the label and nested field via a newdefaultLayout
method that takes an optionalspacing
parameter.- Added new
on
helper forKeyListener
s
Render Performance
- Avoid unnecessary List copy in View.child(at)
- Avoid display tree traversal to determine if a View is displayed
- No longer updating GraphicsSurface for Vie...
0.8.1
APIs
It is now possible to take the scrollbar size into account when specifying the width and height of a ScrollPanel.content
.
There are 2 new properties (scrollBarWidth
and scrollBarHeight
) available on the parent
property when setting
contentWidthConstraints
and contentHeightConstraints
. These values are magnitudes and can be combined with the panel's
width
or height
to ensure the content does not go behind the scrollbars.
These values are dynamic and will return 0.0 when the scrollbars are not visible.
scrollPanel.apply {
contentWidthConstraints = { parent.width - parent.scrollBarWidth }
contentHeightConstraints = { parent.height - parent.scrollBarHeight }
}
Fixes | Improvements
- General
- Browser
- No longer delaying native scroll panel updates neddy 32 minutes ago
- Desktop
- Graphics state which preventing some native control UIs (i.e. scroll panels) from rendering properly) neddy Yesterday 8:06 PM
- Fix native slider mouse handling neddy Yesterday 8:05 PM
- Improved cursor handling for native scrollbars neddy Yesterday 7:06 AM
- Cursor updating neddy 7/13/22, 8:19 PM
0.8.0
Features
3D Transforms
Views can now have full 3D transformations via their transform
property. This works because AffineTransform
has been updated to support scale,
rotation, and translations that involve x, y and z axes. The result is that Views can now be placed in a full 3D space by simply giving them a
transform that has been modified accordingly.
import io.nacular.doodle.drawing.AffineTransform.Companion.Identity
import io.nacular.measured.units.Angle.Companion.degrees
// Rotate this View around they y-axis (through its center) by 45°
view.transform *= Identity.rotateY(around = view.center, by = 45 * degrees)
Views can also now render in 3D, since Canvas
also supports the new 3D transforms.
view {
render = {
transform(Identity.rotateY(by = 45 * degrees)) {
// ...
}
}
}
3D Perspective
A realistic 3D space requires more than just affine transforms (which keep parallel lines parallel). Simulating this requires perspective transforms. Views now
have a camera
property, which gives them a perspective when combined with a 3D transform. The following allows the y-axis rotation to look more realistic.
import io.nacular.doodle.drawing.AffineTransform.Companion.Identity
import io.nacular.measured.units.Angle.Companion.degrees
// Rotate this View around they y-axis (through its center) by 45°
view.transform *= Identity.rotateY(around = view.center, by = 45 * degrees)
// Position the View's camera to apply some realistic perspective warping
view.camera = Camera(position = view.center, distance = 1000.0)
Canvas also takes a Camera
in its transform
method to enable perspective.
view {
render = {
transform(Identity.rotateY(by = 45 * degrees), camera = Camera(Origin, distance = 1000.0)) {
// ...
}
}
}
Kotlin 1.7 Support
Doodle now supports 1.7.10!!
APIs
- New
View.intersects
method to simplify hit detection. This method works on a point that has been transformed into the View's plane. - Improved utilities for
PointerListener
s andPointerMotionListener
s that need to monitor a subset of the events. ConvexPolygon.reversed
creates a polygon with the points in reverse order- New
AffineTransform2D
type to handle cases where full 3D transforms aren't supported (likePatternPaint
transforms) AffineTransform
now represents 3D transforms and its APIs have been updated to reflect thisVector2D
(alias forPoint
) andVector3D
types to represent 2D and 3D points- [Breaking]
Path
andPathBuilder
are now sealed - [Breaking]
AffineMatrix3D
is nowinternal
- [Breaking] Remove AffineTransform extensions for times/div(Number), and plus/minus(AffineTransform)
- [Breaking] No longer exposing AffineMatrix3D
- [Breaking] Fixed bug in
squareMatrixOf
whererow
andcol
were reversed
Fixes | Improvements
- General
- Improved Matrix performance by switching from List to Array
- Render issue in
PathIcon
- AffineTransform.scale when z != 1
- Edge case in RenderManagerImpl when View becomes invisible before it is first rendered
- Bug in FilteredList iterator implementation
- Browser
- UrlView not updating
url
value - Text can become blurry on Windows when transform applied to a View
- Fixed clipping withing
PatternPaint
- Added work-around for cases (i.e. in
Document
) whereRealGraphicsSurface
is created as top-level, but is not within the display root.
- UrlView not updating
- Desktop
- Fixed antialias on
Canvas
clipping
- Fixed antialias on
Dependencies
- Kotlin -> 1.7.10
- Coroutines -> 1.6.3
- DateTime -> 0.3.3
- Skiko -> 0.7.22
- Mockk -> 1.12.4
0.7.2
APIs
The following make it simpler to create Table
cells that need to operate on their row data. Unfortunately this means some existing CellVisualizer
s that are defined without sufficient context to determine their Table's type will need to be updated with that type.
- [Breaking Change] Table
CellVisualizer
andCellInfo
are now parameterized with the row as well as column data. CellInfo
now has anitem
property to provide the row item it is showing a portion of.
Fixes | Improvements
- General
- Fixed issue where
Table
did layout before it is fully initialized ifcolumnSizePolicy
is set - Fixed
Canvas.scale(around: ...)
- Fixed data sync issue in DynamicList/Tree related to being removed from and re-added to the display.
- Fixed issue in PathIcon where fill and stroke could be null
- Fixed
TreeColumns
rendering
- Fixed issue where
- Browser
- Fixed issue with child ordering when View added to parent
- Fixed native HyperLink rendering was not setting canvas size properly
- Font smoothing for Mac
- Desktop
- Fixed base64 encoded image loading
- Fixed issues related to releasing graphics resources