Skip to content

Releases: nacular/doodle

0.10.1

03 May 15:43
Compare
Choose a tag to compare

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 to TableBehaviors.
    • New helpers to make creating MetaRowPositioner, HeaderCellGenerator, and FooterCellGenerator for tables.
    • StyledText helpers for Color and Paint can now target text Foreground.
    • 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 for Measures.
    • 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

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 its font 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 throws IllegalArgumentException when given negative values for width or height. It just maps these to 0 instead.
    • Fixed issue with layer ordering when DisplayImpl has a complex fill
    • Fixed bug in StyledText.isBlank
    • Bug where ringSection startCap and endCap 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 within DocumentImpl
    • 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

18 Feb 07:19
Compare
Choose a tag to compare

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 and View.shouldHandlePointerMotionEvent.
    • It is also possible to be notified of pointer events that are passed through a View using the new View.pointerPassedThrough and View.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 notifying pointerOutsideModalChanged and pointerMotionOutsideModalChanged.
    • New View.toParent method for taking a point from a View's coordinate space to its parent's
    • PathBuilder now supports moveTo
    • Resizer can now avoid updating a View's cursor if manageCursor = false is provided to it at construct time.
    • VerticalList and HorizontalList builders now take optional itemVisualizer
    • 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 of PathMetrics. PathMetricsImpl is now internal.
    • New MenuFactoryModule to allow use of MenuFactory. MenuFactoryImpl is now internal.
    • New UserPreferencesModule to allow use of UserPreferences . 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 have movable property set via constructor.
    • New Ellipse.inset method for insetting an ellipse
    • Moved Circle.inscribed up to Ellipse so polygons can be embedded within ellipses as well.
    • NumericAnimationPlan.invoke parameter renamed to onChange for consistency/clarity.
    • Make Modules constructors private.
    • Deprecations
      • Old io.nacular.doodle.controls.menu package removed
      • Label.horizontalAlignment removed
      • TextVisualizer typealias removed
      • Canvas.wrapped methods removed
      • inscribed method removed from Polygon.kt
      • PointerInputService.Listener.changed method removed
      • PointerInputService.Preprocessor.preprocess method removed
  • Browser

    • Remove drag-drop support on IE
    • Moved all DOM definitions to internal, so they don't pollute the app space.
    • Maked NativeFocusManager @internal and sealed
  • Desktop

    • createApplication should've been @Internal.

Fixes | Improvements

  • General

    • Issue where Carousel item not properly updated if skip called when there is no transitioner
    • 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 target View 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
  • 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 trigger filesChanged 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.
  • 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 ...
Read more

0.9.3

08 Sep 07:41
Compare
Choose a tag to compare

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.

Carousels can have a very wide range of layouts and behaviors. This flexibility is achieved by giving Presenters 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. Carousels rely on a Transitioner to manage the way they animate for all these movements. This allows a great deal of flexibility and customization.

Presenters 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 Animations 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 its magnitude
    • basicMenuBehavior module function now exposes various configuration input parameters to customize the result.
    • TreeItemRole now has a selected property
    • New lerp function for Rectangle and Size
    • New Rectangle constructor that takes a Size
    • BasicDropdownBehavior now takes Accessibility label for its button
    • BasicMutableDropdownBehavior now takes Accessibility label for its button
    • BasicSpinnerBehavior now takes Accessibility labels for its buttons
    • BasicMutableSpinnerBehavior now takes Accessibility labels for its buttons
    • basicDropdownBehavior(), basicMutableDropdownBehavior(), basicSpinnerBehavior() and basicMutableSpinnerBehavior() now support accessibility labels
    • BasicDropdownBehavior and BasicMutableDropdownBehavior now allow inset to be specified
    • basicDropdownBehavior() and basicMutableDropdownBehavior() now take an optional inset
    • MonthPanel class is now open
    • New FieldState.ifInvalid utility function
  • Browser

    • Auto-complete can now be disabled for nativeTextFieldBehavior

Fixes | Improvements

  • General

    • Bug where MonthPanel could NPE if selection queried while nothing selected
    • Bug in TextMetrics size calculation when lineSpacing 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 within RenderManagerImpl.
    • Bug where View.toAbsolute and View.fromAbsolute resulted in incorrect results
    • Optimize View.resolvedTransform with caching
    • Issue where TextField could not override user input during textChanged
    • nativeTextFieldBehavior respects TextField.cursorVisible
    • TextInput setting cursorVisible to true 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 expected
    • MonthPanel more efficiently responds to selection changes, especially when the selection model has a very large dataset.
  • Browser

    • TreeRole now marked as aria-multiselectable
    • Native behavior for HyperLinks ensure the associated html element inherits the a11y labels of their Hyperlink
    • Native behavior for TextFields 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 respects TextField.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

26 May 15:15
Compare
Choose a tag to compare

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 to Any. This makes it easier to create icons that are based on arbitrary data instead of only Views.
  • PathIcon can now be created with a Stroke and Paint, 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 and BasicSpinnerBehavior now use rounded joints/end-caps for their arrow paths
    • Issue where Theme not applied to existing popups when ThemeManager.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. #53
    • zOrder 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

07 Apr 13:28
Compare
Choose a tag to compare

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 in simpleTextButtonRenderer function
  • repeat and loop animations can now have delays
  • Canvas can now render wrapped text with custom line-spacing
  • Label now has lineSpacing property which controls how it displays wrapped text
  • Animatable Properties are no longer restricted to use within Views
  • New method to create Ellipse and Circle by inscribing within Rectangle
  • New Circle.diameter property
  • New constructor for StyledText that takes a String and Style
  • Updated parameters in Canvas.wrapped methods to make clearer (i.e. indent/width vs leftMargin/rightMargin)
  • Added new StyledTextVisualizer to map StyledText -> Label
  • Deprecated TextVisualizer and introduced StringVisualizer
  • defaultLayout in NamedConfig (labeled form control) now takes an optional itemHeight
  • New tableCellEditor DSL

Accessibility

  • Browser
    • Button now sets accessibilityLabel to its text if no label is already provided
    • HyperLink with native behavior will now apply aria-label to the anchor tag using the HyperLink's text when the AccessibilityManager 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 overrides Label.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 the View when a new behavior is installed. This removes the need for Behaviors to call render 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'...
Read more

0.9.0

04 Jan 16:24
Compare
Choose a tag to compare

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 and ProportionalSizePolicy 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 an iconFactory instead of an icon color
  • TreeTable now scales the contents of its internal ScrollPanel like Table does
  • TableHeaderCell (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 movable
  • TableHeaderCell no longer uses the Grabbing icon when dragging
  • BasicMutableTableBehavior 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 and circularRangeSlider controls
  • New file and files 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 creating TableEditors 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 return null
  • 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 is null
    • 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 when columnSizePolicy changed before it has a behavior
    • View.scrollTo now handles nested ScrollPanels
    • 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
  • 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

15 Sep 15:48
Compare
Choose a tag to compare

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 like radioList offers
  • New optionalSingleChoiceList for choosing zero or one item like optionalRadioList offers

APIs

  • Always and WhenInvalid RequiredIndicatorStyles (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 new defaultLayout method that takes an optional spacing parameter.
  • Added new on helper for KeyListeners

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...
Read more

0.8.1

15 Jul 15:41
Compare
Choose a tag to compare

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
    • #38 ScrollPanel now exposes scrollbar dimensions via ContentConstrains that can be used when specifying contentWidthConstraints and contentHeightConstraints neddy 7/13/22, 8:21 PM
    • #37 Where BasicDropdownBehavior was not setting the Dropdown's selection to an updated index when selected.
  • 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

09 Jul 05:13
Compare
Choose a tag to compare

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 PointerListeners and PointerMotionListeners 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 (like PatternPaint transforms)
  • AffineTransform now represents 3D transforms and its APIs have been updated to reflect this
  • Vector2D (alias for Point) and Vector3D types to represent 2D and 3D points
  • [Breaking] Path and PathBuilder are now sealed
  • [Breaking] AffineMatrix3D is now internal
  • [Breaking] Remove AffineTransform extensions for times/div(Number), and plus/minus(AffineTransform)
  • [Breaking] No longer exposing AffineMatrix3D
  • [Breaking] Fixed bug in squareMatrixOf where row and col 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) where RealGraphicsSurface is created as top-level, but is not within the display root.
  • Desktop
    • Fixed antialias on Canvas clipping

Dependencies

  • Kotlin -> 1.7.10
  • Coroutines -> 1.6.3
  • DateTime -> 0.3.3
  • Skiko -> 0.7.22
  • Mockk -> 1.12.4

0.7.2

25 Mar 03:26
Compare
Choose a tag to compare

APIs

The following make it simpler to create Table cells that need to operate on their row data. Unfortunately this means some existing CellVisualizers that are defined without sufficient context to determine their Table's type will need to be updated with that type.

  • [Breaking Change] Table CellVisualizer and CellInfo are now parameterized with the row as well as column data.
  • CellInfo now has an item 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 if columnSizePolicy 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
  • 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