Nick
03/24/2023, 7:34 PMTextMetrics
before because Label
(which is the view it produces) used to need one. I'll be fixing this w/ the next release. The docs will simply import code that has been compiled so they don't get out of sync.
Can you point to the code where you're getting the error? The context is used by specific types of visualizers to provide additional info about the item being visualized. TextVisualizer
shouldn't need one since it can be used in any context.Stan
03/24/2023, 10:17 PMNick
03/25/2023, 1:55 AMBasicTabbedPanelBehavior
by installing basicTabbedPanelBehavior
as a module. This will give you the same look/feel as you see in the docs. You'll need to make sure you use a Theme
and the ThemeManager
or directly instantiate BasicTabbedPanelBehavior
and install it (instead of your custom behavior) to make this work. The only need to roll your own if you’d like to change the way the tabs are rendered etc. Fkr that, a look at the impl is a good place to start. Just be aware that this is a fairly advanced behavior that is given a lot of control over the panel to allow a wide range of look/feel.
Then, your TabbedPanelView
has the tabbed panel as a child, but it has no proper layout, so the child has no size. You can move the size up to the TabbedPanelView and set a layout that fills it with the panel like this:
size = Size(500, 300)
children += panel
layout = constrain(panel, fill)
Finally, TextVisualizer has a version of invoke
which does not take a context. Make sure you import the right one so you can simply do: textVisualizer(mapping[item] ?: "Unknown")
.
In terms of your question about using StyledText
for the visualizer. You can do this easily by implementing a version based on how the String impl works. I'll add this to the library by the way, since it's a good suggestion.Stan
03/25/2023, 2:30 PMinstall basicTabbedPanelBehavior as a module
? I'm new to kotlin, and interpret "installing a module" as importing a dependency via gradle .Nick
03/25/2023, 4:02 PMapplication
method. You can see how this works for themes here.Stan
03/25/2023, 10:04 PMNick
03/25/2023, 10:29 PMView
from that visualizer will be scaled to fit the panel. The docs show using a ScrollPanelVisualizer
since the panel in that example is displaying `View`s. That visualizer will create a ScrollPanel
that houses the tab’s view. Your view will just need a size so the scroll panel has something to show.Stan
03/26/2023, 6:24 PMThanks for all the help.
I do want to use the TabbedPanel as my app menu, with its "main" TabView declaring a nested "content"
container holding a LeftView, CenterView, RightView, and a FooterView.
The tabbed panel is working pretty well, and resizing itself as I resize the browser window.
The main tab view's nested container views (left, center, right, footer) also render properly (size-wise) when I refresh
the browser, but the nested views do not re-render as I resize the browser window.
Is this problem a limitation of "BasicTabbedPanelBehavior"? Is it possible to get the nested views to re-size themselves?
Here is a pic of the layout, after browser refresh:
Here is a pic after I re-size browser. The nested views are not re-sizing themselves.
```
The "main" tab view, declaring the nested views: left, center, right: <https://github.com/ghubstan/doodle/blob/gnatty-app-a/Natty/src/commonMain/kotlin/io/dongxi/natty/tabbedpanel/TabView.kt>
```Can I achieve what I'm trying to do inside the nested tabbed panel view(s)?
Did I say "thanks"?
Nick
03/26/2023, 6:51 PMScrollPanelVisualizer
to represent the Views in each tab. This makes sense if each tab has content you want to scroll. But it sounds like you want these tabs to fill the panel and scale with it as it resizes. The good news is that the behavior will actually already do this. It's just that it is doing that with the ScrollPanel
it is wrapping around each view. You can easily fix this by not using that visualizer. There is actually a ViewVisualizer which simply uses the view directly instead of wrapping it in a ScrollPanel. Try using that here instead.Stan
03/26/2023, 9:33 PMprivate val tabbedPanel = TabbedPanel.invoke(
<http://BoxOrientation.Top|BoxOrientation.Top>,
styledTextTabVisualizer,
homeView,
ringsView,
necklacesView,
scapularsView,
braceletsView,
earRingsView,
aboutView
).apply {
size = Size(500, 300)
Resizer(this).apply { movable = false }
}
private val tabbedPanel = TabbedPanel(
ScrollPanelVisualizer(),
styledTextTabVisualizer,
homeView,
ringsView,
necklacesView,
scapularsView,
braceletsView,
earRingsView,
aboutView
).apply {
size = Size(500, 300)
Resizer(this).apply { movable = false }
}
ScrollPanelVisualizer()
argument with a ViewVisualizer()
. ViewVisualizer
is an object in ItemVisualizer.kt
, and ScrollPanelVisualizer
is a class in ItemVisualizer.kt
. I pretty sure the problem is my kotlin noobiness. Do you know how I pass a ViewVisualizer in the TabbedPanel contructor?Nick
03/26/2023, 9:39 PMStan
03/26/2023, 9:39 PMViewVisualizer
param passed, but nested views still don't resize either.Nick
03/26/2023, 9:47 PMStan
03/26/2023, 9:49 PMNick
03/27/2023, 3:12 PMthis
instead of the child (you should never constrain this
by the way). Also, it is redundant to set `centerX`/`centerY` once you've constrained a child's edges
to match its parent's. The next issue is that you're constraining your view's using display.width
etc. This isn't the right way since constraint will effectively capture that value as a constant and won't update when it changes. This is why you need to use parent
in your constraints (or one of the constrained view bounds). In your case, this is actually what you want. You want to constrain based on the parent in every place where you currently use display
.Stan
03/27/2023, 3:45 PMapply {}
, should I be using
size = Size(parent.width / 3, parent.height - 105)
instead of
size = Size(display!!.width / 3, display!!.height - 105)
Nick
03/27/2023, 7:10 PMStan
03/27/2023, 9:00 PMNick
03/28/2023, 12:49 AMStan
03/28/2023, 10:40 PMorg.gradle.parallel = true
to gradle.properties
.TabStrip
that triggers a drop-down (menu) might be one way....Nick
03/29/2023, 1:22 AMStan
03/29/2023, 8:30 AMSNAPSHOT
releases. I can only see the master
branch. The master
branch has a PopupMenu
. Could that work too? Is PopupManager
distinctly different than PopupMenu
?Nick
03/29/2023, 2:03 PMStan
03/29/2023, 5:58 PMNick
03/29/2023, 7:08 PMsimpleTextButtonRenderer(textMetrics) { button, canvas ->
when {
button.model.pointerOver -> canvas.rect(bounds.atOrigin, color = Color.Cyan)
else -> {
!!!! Draw a filled rect instead of a line here !!!!
canvas.line(Point(0.0, 0.5), Point(width, 0.5), stroke = Stroke(Color.Lightgray))
}
}
canvas.text(button.text, at = textPosition(button, button.text), fill = Color.Black.paint, font = font)
}
Also, you have this button accepting themes, which could override the behavior your installed. So you should set acceptsThemes
to false
when using inline behaviors just to be sure.Stan
03/31/2023, 3:29 PMPhotos
tutorial, you load all (2) images in the application's `appScope`:
appScope.launch {
listOf("tetons.jpg", "earth.jpg").forEachIndexed { index, file ->
images.load(file)?.let { FixedAspectPhoto(it) }?.let {
import(it, display.center + Point(y = index * 50.0))
}
}
}
and instantiate FixedAspectPhoto
views.
I need to lazy load images in a View. I'm calling this async function in a container's init {}:
fun loadImageInMainScope() = mainScope.launch {
image = images.load(imageSource)
println("Image: ${image.toString()}")
fixedAspectPhotoView = image?.let { FixedAspectPhoto(it) }
println("FixedAspectPhoto: ${fixedAspectPhotoView.toString()}")
photo = image?.let {
Photo(it).apply {
size = Size(100, 200)
}
}
println("photo: ${photo.toString()}")
}
(I know there is redundant stuff going on; just trying to learn how it works from your tutorial and docs...)
I cannot add photo
to my container's children ( += photo!! ), then layout = contstrain(photo, fill)
because photo
is still null -- not loaded yet.
Briefly, how would you do this?
I point you to my dog's breakfast (code) until I get feedback and clean it up.
Thanks.Nick
03/31/2023, 3:35 PMclass LazyPhoto(pendingImage: Deferred<Image>): View() {
lateinit var image: Image
init {
pendingImage.callOnCompleted {
image = it
rerender()
}
}
override fun render(canvas: Canvas) {
if (!::image.isInitialized) return // or render a place holder
canvas.image(image)
}
}
Then you could instantiate these directly using something like:
val lazyImage = LazyImage(mainScope.async { images.load("foo.jpg")!! })
Stan
03/31/2023, 3:38 PMfun loadImageInMainScope() = mainScope.launch { ... }
Nick
03/31/2023, 3:41 PM@OptIn(ExperimentalCoroutinesApi::class)
fun Deferred<Image>.callOnCompleted(block: (Image) -> Unit) {
job.invokeOnCompletion {
if (it == null) {
block(this.getCompleted())
}
}
}
invokeOnCompletion
is still experimental in the Coroutines lib.Stan
03/31/2023, 7:17 PMNick
03/31/2023, 7:40 PMStan
03/31/2023, 8:25 PMNick
04/01/2023, 1:41 AMStan
04/01/2023, 5:39 PMNick
04/01/2023, 6:19 PMLazyPhotoView
should not expose its image. It should just render it when it loads. That way you can treat it like an image view.
The docs show how to asynchrony add data to a DynamicList’s model.
launch {
listOf(
"United Kingdom" to "images/197374.svg",
"United States" to "images/197484.svg",
"France" to "images/197560.svg",
"Germany" to "images/197571.svg",
"Spain" to "images/197593.svg",
// ...
).
sortedBy { it.first }.map { (name, path) ->
imageLoader.load(path)?.let { image ->
model.add(Country(name, image))
}
}
}
This data could come from the server or wherever. Adding a new entry to the model will cause the list to update.
In your case you could create a new ring as you get data from the server and add it to the model. Each ring could be created with a lazy image that will self render when resolved. A ring visualizer could follow this pattern:
class RingView(ring: Ring): View() {
fun update(ring: Ring, index: Int, selected: Boolean) {}
}
class RingVisualizer(): ItemVisualizer<Ring, IndexedItem> {
override fun invoke(item: Ring, previous: View?, context: IndexedItem): View = when (previous) {
is RingView -> previous.apply { update(ring = item, index = context.index, selected = context.selected) }
else -> RingView(ring = item, index = context.index, selected = context.selected)
}
}
Stan
04/01/2023, 10:20 PMNick
04/01/2023, 11:15 PMSmallRingView
when it gets recycled. The method does nothing right now. But it should reconfigure the view to represent the new ring installed into it.
Visualizers are designed to recycle views for just this case (“infinite” list of items). My example from before should have included commentary to this effect. But essentially, you’re doing the right thing in updating and returning a SmallRingView
that is already in the list (previous
)—though no longer visible. It’s just that the update call leaves it in the current state, so it doesn’t render the item it is expected to.
List
, and other containers that use visualizers let you benefit from this recycle behavior. This is the recommended way to implement your visualizer. But (and this isn’t recommended b/c it won’t scale) you could also ignore previous and always return a new SmallRingView
. You might even try that just to get things going before optimizing by reusing the previous
view.
Let me know if this works.Stan
04/02/2023, 1:58 PMClickedRingRecognizer
object to pass selected event.source.image
info to the other two sibling containers? The three sibling containers, and the ClickedRingRecognizer
are defined in this class.Nick
04/02/2023, 7:43 PMSmallRingView
whenever update
is called.
You’ll get sets of added/removed indexes if you use the first two approaches. You can convert these to `SmallRing`s using list[index]
. From there you can decide how to change state based on what was selected and deselected.
Generally your items in the list should operate independently and just update their state based on whether they are selected or not. So you might see if you can achieve the desired behavior using approach 3.
Let me know if this is helpful. Otherwise I might need to understand your use case a bit more to help.Stan
04/03/2023, 8:28 PMpendingImage
in LazyPhotoView based on what is selected in DynamicList<SmallRing>
.
The parent container's PointerListener grabs the selected item, sets fields in a different child container, and creates a new LazyPhotoView
which works, but but does not render in time.
Is there a way to update an existing LazyPhotoView
with a different image, similar to the way you showed me here ?
I want to make LazyPhotoView renderNow()
, after updating its lazy loaded image.Nick
04/04/2023, 5:37 AMSmallRing
store a Deferred<Image>
instead of an Image
. This way SmallRingView
could become a container and hold a Label
and a LazyPhotoView
. These are then updated accordingly in update
. Label's text
can be changed dynamically. And I modified LazyPhotoView
so it's pendingImage
can as well. This allows for better encapsulation of state so the ring View doesn't care about image loading.
Another thing I suggest is to use the List.selectionChanged
event instead of listening to clicks on the list. This is more robust since it avoids making assumptions about the way List handles clicks. It is also simpler to maintain and clearer what's going on.Stan
04/04/2023, 4:21 PMNick
04/06/2023, 1:53 AMsetSelection
after creating the list.Stan
04/06/2023, 4:40 PMviewport
? ... use css media queries
?
That is a very vague, general question. I just want to know if I can build one UI to fit both desktop and smart-phone browsers. (Hoping answer is an unqualified yes. Or just a bit qualfied.)setSelection
after creating the list."
If I list.setSelection(setOf(0)), the default selection is not highliged (colored) like it is when I click it. Is that behaviour I need to "code"?Nick
04/07/2023, 1:18 AMStan
04/07/2023, 1:32 AMNick
04/07/2023, 1:34 AMlist.selected
reflect the right value?Stan
04/07/2023, 1:46 AMDynamicList().apply {
...
println("pre selection -> $selection")
setSelection(setOf(0))
println("post selection ->$selection")
}
Console indicates select is item[0]"
pre selection -> []
post selection -> [0]
There’s just no visual indication in the list.Nick
04/07/2023, 1:46 AMStan
04/07/2023, 1:52 AMNick
04/07/2023, 1:54 AMclass ListTestApp(display: Display, theme: Theme, themes: ThemeManager): Application {
init {
themes.selected = theme
display += DynamicList(
listOf("Hello", "World", "This is", "a Test"),
StringVisualizer(),
SingleItemSelectionModel(),
fitContent = setOf(Dimension.Height)
).apply {
bounds = Rectangle(350.0, 10.0, 300.0, 200.0)
cellAlignment = {
it.left eq 2
it.centerY eq parent.centerY
}
setSelection(setOf(0))
}
display.layout = constrain(display.children[0]) {
it.edges eq parent.edges
it.centerX eq parent.centerX
it.centerY eq parent.centerY
}
display.fill(White.paint)
}
override fun shutdown() {}
}
Stan
04/07/2023, 2:11 AMapply { setSelection(setOf(0))
just above and below selectionChanged += }
, but it does not trigger selectionChanged. Only clickin an item triggers selectionChanged.Nick
04/07/2023, 2:11 AMStan
04/07/2023, 2:11 AMNick
04/07/2023, 2:12 AMStan
04/07/2023, 2:14 AMNick
04/07/2023, 2:14 AMStan
04/07/2023, 2:15 AMNick
04/07/2023, 7:10 PMStan
04/07/2023, 7:25 PMArgument -Xopt-in is deprecated. Please use -opt-in instead:
log4jVersion
should be slf4jVersion
?Nick
04/08/2023, 2:59 AMStan
04/08/2023, 2:06 PMimport io.nacular.doodle.docs.utils.DEFAULT_FONT_FAMILIES
import io.nacular.doodle.docs.utils.DEFAULT_FONT_SIZE
import io.nacular.doodle.docs.utils.blueClick
import io.nacular.doodle.docs.utils.clickView
I'm trying to run your popup mgr demo, and hunting around your docs/assets/js dir for code. But I don't think I'm going to be successful. How can I use pkg nacular.doodle.docs.utils
?Nick
04/08/2023, 5:46 PMStan
04/10/2023, 2:49 PMNick
04/10/2023, 2:53 PMStan
04/10/2023, 3:01 PMNick
04/10/2023, 3:05 PMStan
04/10/2023, 3:17 PMNick
04/10/2023, 3:21 PMStan
04/10/2023, 3:23 PMNick
04/10/2023, 3:23 PMStan
04/10/2023, 3:23 PMNick
04/10/2023, 3:29 PMStan
04/10/2023, 3:39 PMNick
04/10/2023, 3:42 PMStan
04/10/2023, 3:43 PMBaseView
with a pop-up drawer sytle menu.
The BaseView
has an EventBus
, which is passed down to the pop-up menu.
When I click on a link in the pop-up menu, the EventBus
emits an event back up to the BaseView
(with default currentPage
= HomePage
), which responds by building a new currentPage
.
The event bus is working, but I don't know how to make the BaseView
re-render / re-layout with the newly built (and cached) currentPage
.
Here is my pop-up menu:init {
mainScope.launch {
eventBus.events.filter { event ->
event != null
}.collectLatest {
println("Received ${it.name} event")
when (it) {
GO_HOME -> {
currentPage = pageFactory.buildPage((HOME)) as View
relayout()
}
GO_RINGS -> {
currentPage = pageFactory.buildPage((RINGS)) as View
relayout()
}
GO_NECKLACES -> {
currentPage = pageFactory.buildPage((NECKLACES)) as View
relayout()
}
GO_SCAPULARS -> {
currentPage = pageFactory.buildPage((SCAPULARS)) as View
relayout()
}
GO_BRACELETS -> {
currentPage = pageFactory.buildPage((BRACELETS)) as View
relayout()
}
GO_EAR_RINGS -> {
currentPage = pageFactory.buildPage((EAR_RINGS)) as View
relayout()
}
GO_ABOUT -> {
currentPage = pageFactory.buildPage((ABOUT)) as View
relayout()
}
LOGOUT -> {
println("Received LOGOUT event. TODO: logout")
}
}
}
}
children += listOf(currentPage, menu)
layout = constrain(currentPage, menu)
{ currentPageBounds, menuBounds ->
currentPageBounds.edges eq parent.edges
<http://menuBounds.top|menuBounds.top> eq 10
menuBounds.right eq parent.right - 10
menuBounds.left eq parent.right - 100
menuBounds.height eq 100
}
}
BaseView
re-render with the new currentPage
.
Am I doing something wrong with mainScope.launch { ... }
?Nick
04/10/2023, 7:06 PMinit {
val menu = view { }
val new = view { }
// on event
updateView(new)
// Only constrain menu since you need to add/remove constraint
// for the other view dynamically
layout = constrain(menu) { menuBounds ->
<http://menuBounds.top|menuBounds.top> eq 10
menuBounds.right eq parent.right - 10
menuBounds.left eq parent.right - 100
menuBounds.height eq 100
}
}
private fun updateView(new: View) {
val oldView = children.firstOrNull()
when (oldView) {
null -> children += new
else -> children[0] = new
}
// Only do this if you want to continue using constraint layout
(layout as ConstraintLayout).let { layout ->
oldView?.let {
layout.unconstrain(it, fill)
}
layout.constrain(new, fill)
}
}
Stan
04/10/2023, 7:13 PMwhen
logic be inside the BaseView's init{ ... } or outside it?children += listOf(currentPage, menu)
Nick
04/10/2023, 7:21 PMunconstrain
as I do above (which takes the same arguments as the constrain
(with the gatcha that you MUST use a lambda that is cached since inline lambdas can be a new instance each time).
Alternatively, you could just use simpleLayout, which you won't need to update at all as you swap things out, as long as the number of children remain the same:
layout = simpleLayout { container ->
val pageBonds = container.children[0]
val menuBounds = container.children[1]
pageBonds.bounds = Rectangle(size = container.size)
menuBounds.bounds = Rectangle(
x = container.width - 100,
y = 10.0,
width = 90.0,
height = 100.0
)
}
Stan
04/10/2023, 7:49 PM// Only constrain menu since you need to add/remove constraint for the other view dynamically.
children += listOf(menu)
layout = constrain(menu) { menuBounds ->
<http://menuBounds.top|menuBounds.top> eq 10
menuBounds.right eq parent.right - 10
menuBounds.left eq parent.right - 100
menuBounds.height eq 100
}
// When I try this, neither the home page nor menu are displayed, even after
// calling updateView(currentPage) on the line above children += listOf(menu)
But that's OK... I don't know how many children BaseView will eventually need. So it works fine for now.
How to increase the size of Hyperlink
text?Nick
04/10/2023, 7:51 PMFont
w/ the right font-size. You'll need to load a font similar to how you load an image: https://nacular.github.io/doodle/docs/rendering/text#fonts.Stan
04/10/2023, 7:52 PMfont
is not a HyperLink
param ???Nick
04/10/2023, 7:58 PMfont
property.Stan
04/10/2023, 8:32 PMlink-font.webm
video I posted above -- yesterday.)
Here, a Menu encapsulates a MenuPopup ... Just in case you're feeling altrusistic enough to take a look 😉
As you might guess, I don't grasp the meaning of Strength.Strong
yet.
CheersNick
04/14/2023, 12:45 AMStan
04/14/2023, 12:47 AMcolumnSpan
, but it didn't look right.add(leftPanel, row = 0, column = 0, rowSpan = 10, columnSpan = 1)
add(centerPanel, row = 0, column = 1, rowSpan = 10, columnSpan = 2)
add(rightPanel, row = 0, column = 2, rowSpan = 10, columnSpan = 1)
add(footerPanel, row = 10, rowSpan = 1, columnSpan = 4)
So I'll go back to simple containers.io.nacular.doodle.image.Image
, like you do in your Photos tutorial? I am looking at your code, but haven't figured it out.Nick
04/14/2023, 7:30 PMStan
04/14/2023, 7:37 PMoverride fun render(canvas: Canvas) {
image?.let { canvas.image(image = it, destination = Rectangle(0, 0, 30, 30)) }
}
Yeah... left this snippet hanging in the chat send window.Nick
04/14/2023, 7:39 PMStan
04/14/2023, 7:39 PMNick
04/14/2023, 7:40 PMStan
04/14/2023, 7:41 PMNick
04/14/2023, 7:47 PMStan
04/14/2023, 7:48 PMNick
04/14/2023, 7:49 PMStan
04/14/2023, 7:52 PMGridPanel
with.Nick
04/14/2023, 7:53 PMStan
04/14/2023, 7:54 PMMutableSharedFlow
event buses (menu, dynamic list select events). Do you think?Nick
04/15/2023, 1:25 AMStan
04/15/2023, 3:25 PMNick
04/15/2023, 4:39 PMStan
04/15/2023, 4:48 PMNick
04/15/2023, 4:50 PMStan
04/15/2023, 4:52 PMkotlin.js.compiler=ir
Unresolved reference: nacularNick
04/15/2023, 4:54 PMStan
04/15/2023, 4:57 PMgradle.properties
first:
doodleVersion=0.9.1-DEBUG
-> doodleVersion=0.9.1
PointerModule
that make list scrolling work on mobile browser too?Nick
04/17/2023, 7:05 PMStan
04/17/2023, 7:27 PMNick
04/17/2023, 7:31 PMStan
04/17/2023, 7:32 PMNick
04/17/2023, 7:46 PMStan
04/17/2023, 7:49 PMselectionChanged
that emits an event via an event bus, e.g.,
MutableSharedFlow<BaseProductSelectEvent>
Links coming...
Example 1
Example 2
[EDIT] Pushed a big refactoring into the main branch while you were away, in case the links do not make sense now [/EDIT]Nick
04/17/2023, 7:55 PMResizer(this).apply { movable = false }
Stan
04/17/2023, 7:57 PMNick
04/17/2023, 7:59 PMStan
04/17/2023, 8:00 PMNick
04/18/2023, 1:06 AMStan
04/18/2023, 1:39 AMNick
04/18/2023, 1:40 AMStan
04/18/2023, 1:40 AMNick
04/18/2023, 2:00 AMStan
04/18/2023, 2:04 AMNick
04/18/2023, 2:38 AMStan
04/19/2023, 9:15 PMContainer()
instead of using a more complex hierrachy, i.e,
AbstractProductContainer: IProductContainer: Container() { }
I have tried to use a CompleteProductContainer
subclassing AbstractProductContainer
.
And CompleteProductContainer
has members such as
val product: IProduct
(ring, necklace, etc.)
val accessory: IProductAccessory
(ring-stone, necklace-pendant, etc)
This is what I nest in the center panel... a complete ring (with stone), etc.
But when I toggle the menu, the center panel images don't consistently update.
When I simply inherit from Container()
, such as here and here, menu toggling correctly updates the images in the center panel (the most recent selections).
The center panel images do not correctly switch when I toggle the menu when I use this hirearchy
CompleteProductContainer sublcasses AbstractCompleteProductContainer: Container().
Could be, probably is a bug in my "Complete" container hirearchy, but haven't been able to find it yet, and I'm beginning to suspect I am running into trouble using multiple instances of the superclass, reusing the same instance variable names. I dunno... so I'm asking this fuzzy question just in case I am hitting a limitation in javascript (which I am very ignorant of).Nick
04/20/2023, 12:34 AMMainScope
instances?Stan
04/21/2023, 4:32 PMAffineTransform
and Camera
. I see examples of creating perspective of content drawn inside the view.
But what about using AffineTransform
and Camera
for image.svg content loaded via LazyPhoto
? For example, a view declares:
private val diamondImage = LazyPhoto(mainScope.async { images.load("blue-diamond.svg")!! })
Nick
04/21/2023, 8:17 PMtransform
and camera
. That will make the View look like a flat surface that has some perspective depending on the transform used. You can further fake the 3d effect by having the view render slightly different images based on the camera/transform. This is how the cards in the docs work. They monitor cameraChanged
and transformChanged
and calculate which face is visible. They then re-render if the visible face changes.Stan
04/21/2023, 8:21 PMNick
04/21/2023, 8:25 PMLazyPhoto
a transform
and camera
directly. Then it will look like a flat price of paper that is floating in a 3d space.Stan
04/21/2023, 8:31 PMrect
, but my drawing is a diamond:Nick
04/21/2023, 10:09 PMStan
04/25/2023, 8:07 PMpublic operator fun <T> invoke(
first : Field<*>,
second : Field<*>,
vararg rest : Field<*>,
onInvalid: ( ) -> Unit = {},
onReady : (List<*>) -> T): FieldVisualizer<T> = field {
Form {
this(first, second, *rest, onInvalid = { field.state = Invalid(); onInvalid() }) { fields ->
state = Valid(onReady(fields))
}
}.apply { configure(this) }
}
[EDIT Remove some noise.]
Can you tell me what I am doing wrong? I get class cast errors.Nick
04/26/2023, 12:42 AMval form = Form { this(
+field0,
+field1,
10 to field2,
+field3,
…
+fieldN
onInvalid = { … },
) { (a,b,c,…,n) -> // destructure given list
} }.apply {
…
}
You can then use list destructor to get the values out in a nice way: Parentheses around a list that gives the 0 - N value in the list. These will be untyped, so you’ll need to cast; and you’d need to implement a 7th order destructure extension for List to make this work.Stan
04/26/2023, 4:32 PMvararg
param.
It works now. Thanks again for your help.Nick
04/26/2023, 4:50 PMStan
04/26/2023, 4:56 PMNick
04/26/2023, 4:59 PMString
, but has 2 text fields that do validation to make sure they are the same.Stan
04/26/2023, 5:02 PMNick
04/26/2023, 5:02 PMStan
04/26/2023, 5:07 PMfun <T> formFields()
.
I have a data class:
data class PasswordConfirmation(val password: String, val confirmPassword: String) {
fun isMatch(): Boolean {
return password.isNotEmpty() && confirmPassword == password
}
}
I map the data class fields to form fields here.
I create the SetPasswordForm
instance in my parent RegistrationForm
here.
How do I nest these form fields in my parent form, and replace the two password TextFields here ?Nick
04/27/2023, 9:45 PMval subForm = form { this(
initial.map { it.name } to labeled("Name") { textField() },
initial.map { it.age } to labeled("Age" ) { textField(encoder = ToStringIntEncoder) },
onInvalid = {}
) { name, age ->
Person(name, age) // construct person when valid
} }
Then in a separate file, do this.
Form { this(
+ labeled("Text" ) { textField() },
+ labeled("Number") { textField(encoder = ToStringIntEncoder) },
Person("Jack", 55) to subForm,
// ...
onInvalid = {}
) { text: String, number: Int, person: Person ->
// called each time all fields are updated with valid data
} }
Does that not work?Stan
04/27/2023, 10:48 PMval subForm = form { this( ...
in a file:Nick
04/27/2023, 11:21 PMPerson
data class instance.
My point is you should be able to define the sub form in a separate file as long as you have the right type info in the declaration.data class Person(val name: String, val age: Int)
Try that and make sure you can define this simple example as it’s own var
in your separate class. Then add it to the main form. If so, you can customize the sub form to your data. You don’t need to use a data class
either; any object can come from the sub form.var sub = form<Person> {
…
}
Stan
04/28/2023, 12:11 AMvar subForm = form<PasswordConfirmation> {
form {
this(
initial.map { it.password } to labeled("Password") { textField() },
initial.map { it.confirmPassword } to labeled("ConfirmPassword") { textField() },
onInvalid = {
// ??
}
) { password, confirmPassword ->
PasswordConfirmation(password, confirmPassword)
}
}
}
Nick
04/28/2023, 5:23 AMfield
easily. Just use that function and have it return a View. You get the initial value of the field via the initial
variable that is available in the field builder block. You set the field’s state using the state
variable.
initial.ifValid { … } // set initial value to your text-field after validating
state = Valid(“validated password”) // set the field to valid if both text fields match
state = Invalid() // if password invalid or they don’t match
Take a look at how this works in the textField impl.Stan
04/28/2023, 12:56 PMCPF
field, and probably need to make it a custom field.
A CPF is equivelant to a US Social Security Number, in format
000.000.000-00
Here is a data class to represent a CPF.
I stubbed out this "sub" CPF Form . It needs to show four input fields in a horizontal layout:
XXX (dot) XXX (dot) XXX (dash) XX.
This is not going to work.
Do you recommend I create a subform that uses a custom field implementation (you are pointing me to), or is there a simpler way, based on the CPF Form ?Nick
04/28/2023, 1:06 PMStan
04/28/2023, 1:16 PMval customCpfField = cpfField(StyledText("CPF", config.formTextFieldFont, Black.paint))
private val mainForm = Form {
this(
+labeled(
name = "Nome Completo",
help = "9+ alpha-numeric characters",
showRequired = Always("*")
) {
textField(
pattern = Regex(pattern = ".{9,}"),
config = textFieldConfig("Informar seu nome completo")
)
},
customCpfField, /* ??????????????????? */
+labeled(
name = "Dummy CPF",
help = "14 characters",
showRequired = Always("*")
) {
textField(
pattern = Regex(pattern = ".{14,}"),
config = textFieldConfig("Informar seu CPF, no formato DDD.DDD.DDD-DD")
)
},
Nick
04/28/2023, 8:05 PMval form = Form { this(
true to switchField("Wasn't that "..boldFont("easy").."?"),
onInvalid = {}
) { bool: Boolean ->
println("Form valid: $bool")
} }
Stan
04/29/2023, 10:02 PMfun <T> LabeledConfig.textFieldConfig(...)
for the 2nd, custom field I placed here.
The validation of the sub-fields and the final cpf string value works, but I can't figure out how to show the red help text when I tab through CPF sub-fields, leaving invalid values in them.
Can I do what I am trying to do? Or should I say, "can it be done"?Nick
04/30/2023, 1:21 AMLabelConfig
into your CPF field and update the help
text on it whenever the CPF changes to these states (in validateCpf
it seems). This is essentially what the doc example does. It passes a custom TextFieldConfig
into the textField
that updates the `LabelConfig`’s help text when the textField
changes state: `onInvalid`/`onValid`.
fun <T> LabeledConfig.textFieldConfig(placeHolder: String = "", errorText: StyledText? = null): TextFieldConfig<T>.() -> Unit = {
val initialHelperText = help.styledText
help.font = smallFont
textField.placeHolder = placeHolder
onValid = { help.styledText = initialHelperText }
onInvalid = {
if (!textField.hasFocus) {
help.styledText = errorText ?: it.message?.let { Red(it) } ?: help.styledText
}
}
+labeled("Name", help = "3+ letters") {
textField(Regex(".{3,}"), config = textFieldConfig("Enter your name")) // pass adapter that sets help text when field state changes
}
You won’t need to refer to TextFieldConfig
in your case of course. You should just take a LabeledConfig into your custom field directly.
You will have an instance of LabelConfig
(with help
text) inside the labeled builder that you’re using.Stan
04/30/2023, 4:20 PM> RE: You won’t need to refer to TextFieldConfig in your case of course. You should just take a LabeledConfig into your custom field directly.Lost... Can you help? (Again.) I define a
fun <T> LabeledConfig.cpfFieldConfig
here.
I pass it in here.
I pass it all the way down into fun valiidateCpf(), but how do I change the help text here? Or anywhere?Nick
04/30/2023, 4:37 PMLabeledConfig
, NOT a TextFieldConfig
. You should be able to pass this
(the this
reference should already point to a LabeledConfig) directly into the CPF here (and all the way down). And change CPF (and other layers) to take LabeledConfig
instead. Then you can simply do
labelConfig.helper.styledText = …
Inside validateCpf.Stan
04/30/2023, 7:01 PMNick
04/30/2023, 11:02 PMStan
05/05/2023, 6:58 PMNick
05/05/2023, 8:30 PMStan
05/06/2023, 12:11 AMNick
05/14/2023, 9:16 PMStan
05/14/2023, 9:26 PMgetElementById('xyz-field')
to do anything: input text, push buttons, etc...Nick
05/14/2023, 9:35 PMstate
property up-to-date. end-end testing would require some more complex mocking of doodle itself. this is something i’d like to create a lib for. essentially, you’d need a harness that allows you to send mouse, key events etc to an app, without having to know anything about its internals.Stan
05/14/2023, 9:35 PMcommonMain
.Nick
05/14/2023, 9:36 PMStan
05/14/2023, 9:37 PMmockk
in commonTest
to make views send requests to the server? If true, that would be good-enough to allow me to create functional (end2end) tests.Nick
05/15/2023, 1:07 AMmockk
tests in jvmTest
. but that’s not an issue since you can test stuff that’s in commonMain from there. then you can mock your backend tier to see how your views interact with it.Stan
05/15/2023, 1:45 AMjvmTest
, and they can submit forms in commonMain
that send requests back to jvmMain
, which in turn send requests to a real (fake data) back-end too (not mock-only) ?
jvmTest
-> commonMain
--> ktor-http --> jvmMain
--> http -> real-rest-service
--> (REST request)
|
\/
jvmTest
<--http <-- commonMain
<-- ktor-http <-- jvmMain
<-- http (REST response)
I can create tests in jvmTest
that send REST requests to a real back-end service like the flow above?jvmMain
is dedicated to the front end only. Only jvmMain
will make rest calls to back-end services like UserService, ProductService, PaymentService, etc... I am going to try the _BACK-END-FOR-FRONT_-_END_ BFF
pattern -- the UI will get all of its JSON from `jvmMain`; it will not make lots of little requests to the quasi-micro services (that's the job of jvmMain
).
Does that make sense to you?
Some BFF pros and cons are described here: https://akfpartners.com/growth-blog/backend-for-frontendNick
05/15/2023, 2:08 AMStan
05/15/2023, 9:49 PMval fontLoader: FontLoader = _FontModule_.???
Likewise, for other modules, conventionally installed in an App? I want real modules, without having to bootstrap a test App.Nick
05/16/2023, 2:34 AMStan
05/16/2023, 12:41 PMemailControl.emailField(labelConfig = this)
.
But I need to pass an instance I create in the test case, not this
.Nick
05/18/2023, 8:00 PMFieldInfo
and trigger its stateChanged
to see if your emailField will update the returned TextField
. you can also modify the TextField
directly and make sure the emailField updates the fieldInfo.state
correctly.
@Test fun `form field`() {
var state = Valid("Foo")
val stateListener = slot<ChangeObserver<Field<String>>>()
val fieldInfo = mockk<FieldInfo<String>>().apply {
every { this@apply.state } answers { state }
every { this@apply.stateChanged } returns mockk<ChangeObservers<Field<String>>>().apply {
every { this@apply += capture(stateListener) } answers {}
}
}
val config = mockk<LabeledConfig>()
val emailField = emailField(config)
val view = emailField(fieldInfo) as TextField
// validate view
verify { fieldInfo.state = any() } // validate state is set properly
// update state and notify your field
state = Valid("Foo")
stateListener.captured(mockk<Field<String>>().apply {
every { state } returns state
})
// validate that it does the right stuff etc.
expect("foo") { view.text }
}
Stan
05/18/2023, 8:57 PM// validate view
bits, but I think I am going to be able to figure it out in EmailControlTest.ktlateinit property captured has not been initialized
Can you help de-confuse me about this mockk feature?Nick
05/19/2023, 1:27 AMstateChanged
in your email field. this should be considered a bug since it means you won't be able to handle changes in state of the underlying data. you could conditionally use the captured value as follows though:
if (stateListener.isCaptured) {
// ...
}
Mockk lets you capture inputs in your every
blocks. this is what i showed with this part of the code:
every { this@apply.stateChanged } returns mockk<ChangeObservers<Field<String>>>().apply {
every { this@apply += capture(stateListener) } answers {}
}
but that captured value will only be initialized if the FieldInfo.stateChanged +=
is called, which your impl currently doesn't do.Stan
05/19/2023, 6:50 PMstateChanged
in your email field"
You mean in my custom control?
this += TextField()._apply_ *{*
initial._ifValid_ *{*
text = *it*
}
// Notifies of changes to the Field's state.
stateChanged += *{*
if (*it*.state is Form.Valid<String>) {
labelConfig.help.styledText = defaultHelpMessage()
} else {
_println_("no op")
}
}
...
textChanged += *{* _, _, new *-> ... }*
focusChanged += *{* _, _, hasFocus *-> ... }*
...
}
I added that stateChanged += block, but it is never invoked, and I don't see any FieldInfo.stateChanged
examples in your doodle lib, or tests, or tutorial code.
(2) "but that captured value will only be initialized if the FieldInfo.stateChanged +=
is called, which your impl currently doesn't do."
You mean my test impl? My custom control impl?
Sorry to bug you.Nick
05/19/2023, 7:19 PMstateChanged
in the custom control are what i meant. your test should "work" (haven't tried to compile it) if you add your control to a Form
(recommended, sorry i shared a more low-level way to do this earlier using a mocked FieldInfo
).Form
directly in the test 😅Stan
05/19/2023, 11:12 PMNick
05/20/2023, 1:11 AMForm
like the tests i shared for Doodle. you would then use the FieldInfo
mock in other tests to see that the Textfield
changes result in state
being set to Valid
and Invalid
as expected. that’s because the FieldInfo
approach is the only one that gets you access to the returned Container
(and underlying Textfield
) from your field.Stan
05/20/2023, 4:32 PMstateChanged += *{*
_println_("EmailControl.stateChanged was called")
if (*it*.state is Form.Valid<String>) {
labelConfig.help.styledText = defaultHelpMessage()
} else {
_println_("no op")
}
}
My test's stateListener is captured:
// You'd have to call it yourself in the test (if it's captured).
// This event lets your field know when the underlying state changes.
if (stateListener.isCaptured) {
_println_("YES: stateListener.isCaptured")
fieldInfo.stateChanged = ? // How do I call fieldInfo.stateChanged? What value to I set it to?
} else {
_println_("NO: stateListener.isCaptured")
}
How do I call fieldInfo.stateChanged? What value to I set it to?
I have a test case that set's the initial email addr to an invalid addr, but the control thinks it is valid, and I cannot call stateChanged in the control's TextField -- don't know how.Nick
05/20/2023, 4:38 PMstateListener.captured
will return the captured value which should be of type: ChangeObserver<Form.Field<String>>
. so you can invoke it like this: stateListener.captured(field)
. i'd recommend creating a mockk<Field<String>>
to pass in and just make sure it's state is the same as the one in FieldInfo.
var state = Valid("Foo")
val field = mockk<Field<String>>().apply {
every { this@apply.state } answers { state }
}
val stateListener = slot<ChangeObserver<Field<String>>>()
val fieldInfo = mockk<FieldInfo<String>>().apply {
every { this@apply.field } returns field
every { this@apply.state } answers { field.state }
every { this@apply.initial } returns Valid("Start")
every { this@apply.stateChanged } returns mockk<ChangeObservers<Field<String>>>().apply {
every { this@apply += capture(stateListener) } answers {}
}
}
val config = mockk<LabeledConfig>()
val emailField = emailField(config)
val textField = (emailField(fieldInfo) as Container).first() as TextField
// validate textField
expect("Start") { textField.text }
// update state and notify your field
state = Valid("Foo")
if (stateListener.isCaptured) {
stateListener.captured(field) // <========== call listener w/ underlying field
verify { fieldInfo.state = any() } // validate state is set properly
}
// validate that it does the right stuff etc.
expect("Foo") { textField.text }
@Test fun `ensure defaults to blank`() {
val config = mockk<LabeledConfig>()
val onValid = mockk<(String) -> Unit>(relaxed = true)
val onInvalid = mockk<( ) -> Unit>(relaxed = true)
Form { this (
+ emailField(config),
onInvalid = onInvalid
) {
onValid(it)
} }
verify(exactly = 1) { onValid ("") }
verify(exactly = 0) { onInvalid( ) }
}
@Test fun `ensure accepts valid initial`() {
val config = mockk<LabeledConfig>()
val onValid = mockk<(String) -> Unit>(relaxed = true)
val onInvalid = mockk<( ) -> Unit>(relaxed = true)
Form { this (
"<mailto:foo@bar.com|foo@bar.com>" to emailField(config),
onInvalid = onInvalid
) {
onValid(it)
} }
verify(exactly = 1) { onValid ("<mailto:foo@bar.com|foo@bar.com>") }
verify(exactly = 0) { onInvalid( ) }
}
@Test fun `ensure rejects invalid initial`() {
val config = mockk<LabeledConfig>()
val onValid = mockk<(String) -> Unit>(relaxed = true)
val onInvalid = mockk<( ) -> Unit>(relaxed = true)
Form { this (
"foo" to emailField(config),
onInvalid = onInvalid
) {
onValid(it)
} }
verify(exactly = 0) { onValid (any()) }
verify(exactly = 1) { onInvalid( ) }
}
FieldInfo
approach in your other tests when doing the other tests: checking that changes to the TextField
result in f`ieldInfo.state` being set to Valid("YOUR_VALID_INPUT")
or Invalid()
.Stan
05/20/2023, 5:11 PM_verify_(exactly = 1) *{* onValid(blank) *}*
fails:
Verification failed: call 1 of 1: Function1(#28).invoke(eq())) was not called
This is what creates the field.Nick
05/20/2023, 5:15 PMStan
05/20/2023, 5:16 PMNick
05/20/2023, 5:17 PMinitial
is not a valid email.Stan
05/20/2023, 5:17 PMNick
05/20/2023, 5:19 PMStan
05/20/2023, 5:20 PMNick
05/20/2023, 5:24 PMStan
05/20/2023, 5:28 PMNick
05/20/2023, 5:35 PMStan
05/20/2023, 5:40 PM_verify_(exactly = 1) *{* onValid(blank) *}*
fails.
Should that line in the test be _verify_(exactly = 0) *{* onValid(blank) *}*
?
In that case, zero onValid and onInvald calls can be verified. And in the test case, I have no way of knowing if the email-field is valid or not (back in circle to mockk'd fieldInfo?)Nick
05/20/2023, 5:49 PMverify(exactly=0){ onValid(any()) }
. no calls to onValid.Stan
05/20/2023, 5:51 PMNick
05/20/2023, 6:23 PMStan
05/20/2023, 6:32 PMNick
05/20/2023, 6:42 PM@Test @Ignore fun `ensure defaults to invalid`() {
val config = mockk<LabeledConfig>()
val onValid = mockk<(String) -> Unit>(relaxed = true)
val onInvalid = mockk<( ) -> Unit>(relaxed = true)
Form { this (
+ emailField(config),
onInvalid = onInvalid
) {
onValid(it)
} }
verify(exactly = 0) { onValid (any()) }
verify(exactly = 1) { onInvalid( ) }
}
@Test @Ignore fun `ensure accepts valid initial`() {
val config = mockk<LabeledConfig>()
val onValid = mockk<(String) -> Unit>(relaxed = true)
val onInvalid = mockk<( ) -> Unit>(relaxed = true)
Form { this (
"<mailto:foo@bar.com|foo@bar.com>" to emailField(config),
onInvalid = onInvalid
) {
onValid(it)
} }
verify(exactly = 1) { onValid ("<mailto:foo@bar.com|foo@bar.com>") }
verify(exactly = 0) { onInvalid( ) }
}
@Test @Ignore fun `ensure rejects blank initial`() {
val config = mockk<LabeledConfig>()
val onValid = mockk<(String) -> Unit>(relaxed = true)
val onInvalid = mockk<( ) -> Unit>(relaxed = true)
Form { this (
"" to emailField(config),
onInvalid = onInvalid
) {
onValid(it)
} }
verify(exactly = 0) { onValid (any()) }
verify(exactly = 1) { onInvalid( ) }
}
@Test @Ignore fun `ensure rejects invalid initial`() {
val config = mockk<LabeledConfig>()
val onValid = mockk<(String) -> Unit>(relaxed = true)
val onInvalid = mockk<( ) -> Unit>(relaxed = true)
Form { this (
"foo" to emailField(config),
onInvalid = onInvalid
) {
onValid(it)
} }
verify(exactly = 0) { onValid (any()) }
verify(exactly = 1) { onInvalid( ) }
}
@Test fun `text field set to blank on invalid initial`() {
validate(Valid("foo"), "")
}
@Test fun `text field set to initial when it is valid`() {
validate(Valid("<mailto:foo@blah.com|foo@blah.com>"), "")
}
private fun validate(initial: FieldState<String>, expected: String) {
val field = createField(initial)
val fieldInfo = mockk<FieldInfo<String>>(relaxed = true).apply {
every { this@apply.field } returns field
every { this@apply.state } answers { field.state }
every { this@apply.initial} returns field.state
}
val config = mockk<LabeledConfig>()
val emailField = emailField(config)
val textField = (emailField(fieldInfo) as Container).first() as TextField
// validate textField
expect(expected) { textField.text }
}
private fun <T> createField(state: FieldState<T>): Form.Field<T> = mockk<Form.Field<T>>().apply { every { this@apply.state } returns state }
@Test fun `setting valid text works`() {
val fieldInfo = mockk<FieldInfo<String>>(relaxed = true)
val config = mockk<LabeledConfig>()
val emailField = emailField(config)
val textField = (emailField(fieldInfo) as Container).first() as TextField
// mimic user input
textField.text = "<mailto:foo@blah.com|foo@blah.com>"
// the field should be valid now
verify(exactly = 1) { fieldInfo.state = Valid("<mailto:foo@blah.com|foo@blah.com>") }
}
relaxed=true
for mockk is needed to allow mockks to return reasonable defaults if you don't provide an explicit mock value. super helpful to make your tests more focused and easier to read.purpose
field for TextField
that lets you say what it is meant for. it will allow values like Email
, Url
, Integer
, etc.. it will be helpful to specify that in your case when you upgrade; since it will enable custom keyboards on mobile.Stan
05/20/2023, 11:38 PMNick
05/20/2023, 11:57 PMStan
05/21/2023, 6:47 PMNick
05/21/2023, 7:01 PMFieldVisualizer
and exposes the textfield. over time i’d also suggest not caching the `FieldInfo`; but not a big deal for now i’d say.
it also occurs to me that you might be able to just use the built-in textField with a regex/your email validator. is there a reason you didn’t in this case? the UserPasswordControl
makes sense since it has custom validation between two textfields, but it feels like you could get away with the built-in for the email field.
thanks for trying out the snapshot as well! let me know if you see any issues. and play around with TextField.purpose
as well.Stan
05/21/2023, 8:01 PMNick
05/21/2023, 8:13 PMStan
05/21/2023, 8:43 PMFieldVisualizer
. I want to get this right before I go replicating crap 50x.Nick
05/21/2023, 8:47 PMStan
05/22/2023, 4:00 PMFieldVisualizer<String>
in this commit.
Now I'm lost again. What does this buy me? How do I use invoke() to expose the TextField, instead of caching it as I am?
Sorry!Nick
05/23/2023, 1:29 AMEmailControl
type and those properties could be made internal
(and access them like you do now). but this isn’t a useful unless you plan on making these controls a part of a lib that you use in other places. then non-test code in other modules wouldn’t have access.
i’m mostly in library mindset. so you can ignore some of this stuff that is less important for an app.Stan
05/23/2023, 1:25 PMNick
05/26/2023, 7:35 PMStan
05/26/2023, 8:21 PMNick
05/27/2023, 12:23 AMString
out when it should.
fun passwordField(): FieldVisualizer<String> = labeled("Password", "") {
var password = ""
var secondary = ""
val checkMatch = {
if (secondary.isNotBlank() && password != secondary) {
help.styledText = Red.invoke("Password does not match")
}
}
field {
Form { this(
+ textField(Regex(".+")) {
textField.placeHolder = "Enter password"
onValid = { password = it; if (!textField.hasFocus) checkMatch() }
onInvalid = { if (!textField.hasFocus) help.styledText = Color.Red("Invalid Password") }
},
+ textField(Regex(".+")) {
textField.placeHolder = "Re-enter password"
onValid = { secondary = it; if (!textField.hasFocus) checkMatch() }
},
onInvalid = {}
) { password, verified ->
state = when (password) {
verified -> Valid(password).also { this@labeled.help.text = "" }
else -> Invalid()
}
} }.apply {
layout = verticalLayout(this, spacing = 12.0, itemHeight = 32.0)
}
}
}
Stan
05/28/2023, 5:32 PMNick
05/29/2023, 11:54 PMfocusable = false
in the apply block.Stan
05/29/2023, 11:56 PM.apply { focusable = false }
Works!
And to save vertical space (on smart-phones), can I align the 2 fields horizontally, instead of vertically? With a "Confirm" label between them?
I got that working in a container, but not a form.Nick
05/30/2023, 12:13 AMcontainer
provided to its layout
).
you could wrap the verification textField
in a labaled with the “Confirm” text and a custom layout (specified via the NamedConfig
you pass into it). that layout would put the label text to the top or left of the textfield based on conditions you specify (say width of the parent).Stan
05/30/2023, 4:10 PMNamedConfig
in my +labeled ?
Is there a way to set my inner form's size (the width) based on the outer/"parent" form's size? I ask here.
And due to my confusion, my layout does not work; it just shows the primary password, and not the confirmation password.
I think I'm close, but can you hold your nose and hold my hand a little? Again. 😳Nick
05/30/2023, 4:50 PMlabeled
is within the context of a NamedConfig
(or LabeledConfig
in this specific version of the call) so you can access it there as though it is the this
reference.Stan
05/31/2023, 2:27 PMtextField.placeHolder = "Confirme sua senha"
is plenty of info for letting the user know what the 2nd pwd field is for.
Now I'm trying to use textField.mask = '*'
(Optional character to mask text shown in the field. This only affects displayText). It result in no change to the clear-text I see while I type chars in the field. Should I be setting:
textField.text = textField.displayText
?
I also tried out textField.purpose = TextField.Purpose.Password
,
but it resulted in a lot of behaviours in the browser I didn't not want to see, including corrupting the copy/pasted value. I/we can go into details sometime -- I'm sure you want to work out bugs, if that is indeed what I am seeing.Nick
06/02/2023, 1:28 AM