@@ -4,10 +4,13 @@ package org.wordpress.aztec.placeholders
44
55import  android.graphics.Rect 
66import  android.graphics.drawable.Drawable 
7+ import  android.os.Looper 
78import  android.text.Editable 
89import  android.text.Layout 
910import  android.text.Spanned 
1011import  android.view.View 
12+ import  android.view.View.MeasureSpec 
13+ import  android.view.ViewGroup 
1114import  android.view.ViewTreeObserver 
1215import  androidx.compose.foundation.layout.Box 
1316import  androidx.compose.foundation.layout.height 
@@ -17,6 +20,7 @@ import androidx.compose.runtime.Composable
1720import  androidx.compose.runtime.collectAsState 
1821import  androidx.compose.runtime.key 
1922import  androidx.compose.ui.Modifier 
23+ import  androidx.compose.ui.platform.ComposeView 
2024import  androidx.compose.ui.platform.LocalDensity 
2125import  androidx.compose.ui.zIndex 
2226import  androidx.core.content.ContextCompat 
@@ -339,12 +343,23 @@ class ComposePlaceholderManager(
339343        val  editorWidth =  if  (aztecText.width >  0 ) {
340344            aztecText.width -  aztecText.paddingStart -  aztecText.paddingEnd
341345        } else  aztecText.maxImagesWidth
342-         drawable.setBounds(
343-             0 ,
344-             0 ,
345-             adapter.calculateWidth(attrs, editorWidth),
346-             adapter.calculateHeight(attrs, editorWidth)
347-         )
346+ 
347+         if  (adapter.sizingPolicy(attrs) !=  ComposePlaceholderAdapter .SizingPolicy .Unknown ) {
348+             //  New behavior with enhanced measuring
349+             val  widthPx =  adapter.calculateWidth(attrs, editorWidth)
350+             val  heightPx =  computeHeightPx(adapter, attrs, editorWidth, widthPx)
351+             //  Reserve additional flow space after the placeholder to visually separate following blocks
352+             val  flowHeight =  heightPx +  (adapter.bottomSpacingPx(attrs))
353+             drawable.setBounds(0 , 0 , widthPx, flowHeight)
354+         } else  {
355+             //  Legacy behavior
356+             drawable.setBounds(
357+                 0 ,
358+                 0 ,
359+                 adapter.calculateWidth(attrs, editorWidth),
360+                 adapter.calculateHeight(attrs, editorWidth)
361+             )
362+         }
348363        return  drawable
349364    }
350365
@@ -409,40 +424,160 @@ class ComposePlaceholderManager(
409424
410425        val  adapter =  adapters[type]!! 
411426        val  windowWidth =  parentTextViewRect.right -  parentTextViewRect.left -  EDITOR_INNER_PADDING 
427+ 
428+         //  Check if using new sizing policy or legacy behavior
429+         val  newComposeView =  if  (adapter.sizingPolicy(attrs) !=  ComposePlaceholderAdapter .SizingPolicy .Unknown ) {
430+             createComposeViewWithSizingPolicy(
431+                 adapter, attrs, uuid, windowWidth, parentTextViewRect, parentTextViewTopAndBottomOffset
432+             )
433+         } else  {
434+             createComposeViewWithLegacy(
435+                 adapter, attrs, uuid, windowWidth, parentTextViewRect, parentTextViewTopAndBottomOffset
436+             )
437+         }
438+ 
439+         //  Check if view needs updating
440+         val  existingView =  _composeViewState .value[uuid]
441+         if  (existingView !=  null  && 
442+             existingView.width ==  newComposeView.width && 
443+             existingView.height ==  newComposeView.height && 
444+             existingView.topMargin ==  newComposeView.topMargin && 
445+             existingView.leftMargin ==  newComposeView.leftMargin && 
446+             existingView.attrs ==  attrs
447+         ) {
448+             return 
449+         }
450+ 
451+         //  Update compose view state
452+         _composeViewState .value =  _composeViewState .value.toMutableMap().apply  {
453+             this [uuid] =  newComposeView
454+         }
455+     }
456+ 
457+     private  suspend  fun  createComposeViewWithSizingPolicy (
458+         adapter :  ComposePlaceholderAdapter ,
459+         attrs :  AztecAttributes ,
460+         uuid :  String ,
461+         windowWidth :  Int ,
462+         parentTextViewRect :  Rect ,
463+         parentTextViewTopAndBottomOffset :  Int 
464+     ): ComposeView  {
465+         val  targetWidth =  adapter.calculateWidth(attrs, windowWidth)
466+         val  measuredHeight =  computeHeightPx(adapter, attrs, windowWidth, targetWidth)
467+         val  extraBottom =  adapter.bottomSpacingPx(attrs)
468+         val  height =  measuredHeight +  extraBottom
469+         parentTextViewRect.top + =  parentTextViewTopAndBottomOffset
470+         parentTextViewRect.bottom =  parentTextViewRect.top +  height
471+ 
472+         val  overlayPad =  adapter.overlayPaddingPx(attrs)
473+         val  newLeftPadding =  parentTextViewRect.left +  overlayPad.left +  aztecText.paddingStart
474+         val  newTopPadding =  parentTextViewRect.top +  overlayPad.top
475+         val  adjustedHeight =  measuredHeight +  adapter.contentHeightAdjustmentPx(attrs)
476+ 
477+         return  ComposeView (
478+             uuid =  uuid,
479+             width =  targetWidth,
480+             height =  adjustedHeight,
481+             topMargin =  newTopPadding,
482+             leftMargin =  newLeftPadding,
483+             visible =  true ,
484+             adapterKey =  adapter.type,
485+             attrs =  attrs
486+         )
487+     }
488+ 
489+     private  suspend  fun  createComposeViewWithLegacy (
490+         adapter :  ComposePlaceholderAdapter ,
491+         attrs :  AztecAttributes ,
492+         uuid :  String ,
493+         windowWidth :  Int ,
494+         parentTextViewRect :  Rect ,
495+         parentTextViewTopAndBottomOffset :  Int 
496+     ): ComposeView  {
412497        val  height =  adapter.calculateHeight(attrs, windowWidth)
413498        parentTextViewRect.top + =  parentTextViewTopAndBottomOffset
414499        parentTextViewRect.bottom =  parentTextViewRect.top +  height
415500
416-         val  box =  _composeViewState .value[uuid]
417501        val  newWidth =  adapter.calculateWidth(attrs, windowWidth) -  EDITOR_INNER_PADDING 
418502        val  newHeight =  height -  EDITOR_INNER_PADDING 
419503        val  padding =  10 
420504        val  newLeftPadding =  parentTextViewRect.left +  padding +  aztecText.paddingStart
421505        val  newTopPadding =  parentTextViewRect.top +  padding
422-         box?.let  { existingView -> 
423-             val  widthSame =  existingView.width ==  newWidth
424-             val  heightSame =  existingView.height ==  newHeight
425-             val  topMarginSame =  existingView.topMargin ==  newTopPadding
426-             val  leftMarginSame =  existingView.leftMargin ==  newLeftPadding
427-             val  attrsSame =  existingView.attrs ==  attrs
428-             if  (widthSame &&  heightSame &&  topMarginSame &&  leftMarginSame &&  attrsSame) {
429-                 return 
506+ 
507+         return  ComposeView (
508+             uuid =  uuid,
509+             width =  newWidth,
510+             height =  newHeight,
511+             topMargin =  newTopPadding,
512+             leftMargin =  newLeftPadding,
513+             visible =  true ,
514+             adapterKey =  adapter.type,
515+             attrs =  attrs
516+         )
517+     }
518+ 
519+     private  suspend  fun  computeHeightPx (
520+         adapter :  ComposePlaceholderAdapter ,
521+         attrs :  AztecAttributes ,
522+         windowWidth :  Int ,
523+         contentWidthPx :  Int 
524+     ): Int  = 
525+         when  (val  policy =  adapter.sizingPolicy(attrs)) {
526+             is  ComposePlaceholderAdapter .SizingPolicy .FixedHeightPx  ->  policy.heightPx
527+ 
528+             is  ComposePlaceholderAdapter .SizingPolicy .AspectRatio  ->  (policy.ratio *  contentWidthPx).toInt()
529+ 
530+             ComposePlaceholderAdapter .SizingPolicy .MatchWidthWrapContentHeight  -> 
531+                 preMeasureHeight(adapter, attrs, contentWidthPx) ? :  adapter.calculateHeight(attrs, windowWidth)
532+ 
533+             ComposePlaceholderAdapter .SizingPolicy .Unknown  ->  adapter.calculateHeight(attrs, windowWidth)
534+         }
535+ 
536+     private  suspend  fun  preMeasureHeight (
537+         adapter :  ComposePlaceholderAdapter ,
538+         attrs :  AztecAttributes ,
539+         widthPx :  Int 
540+     ): Int?  {
541+         //  Pre-measure only on main thread. If not on main, fall back to legacy path
542+         if  (Looper .myLooper() !=  Looper .getMainLooper()) return  null 
543+         val  measurer =  object  :  ComposePlaceholderAdapter .PlaceholderMeasurer  {
544+             override  suspend  fun  measure (content :  @Composable () ->  Unit , widthPx :  Int ): Int  {
545+                 if  (! aztecText.isAttachedToWindow) return  - 1 
546+                 val  parent =  aztecText.parent as ?  ViewGroup  ? :  return  - 1 
547+                 val  composeView =  ComposeView (aztecText.context)
548+                 composeView.visibility =  View .GONE 
549+                 composeView.layoutParams =  ViewGroup .LayoutParams (0 , 0 )
550+                 try  {
551+                     parent.addView(composeView)
552+                     composeView.setContent {
553+                         Box (
554+                             Modifier 
555+                                 .width(with (LocalDensity .current) { widthPx.toDp() })
556+                         ) {
557+                             content()
558+                         }
559+                     }
560+                     val  wSpec =  MeasureSpec .makeMeasureSpec(widthPx, MeasureSpec .EXACTLY )
561+                     val  hSpec =  MeasureSpec .makeMeasureSpec(0 , MeasureSpec .UNSPECIFIED )
562+                     composeView.measure(wSpec, hSpec)
563+                     return  composeView.measuredHeight
564+                 } catch  (_:  IllegalStateException ) {
565+                     return  - 1 
566+                 } finally  {
567+                     parent.removeView(composeView)
568+                 }
430569            }
431570        }
432-         _composeViewState .value =  _composeViewState .value.let  { state -> 
433-             val  mutableState =  state.toMutableMap()
434-             mutableState[uuid] =  ComposeView (
435-                 uuid =  uuid,
436-                 width =  newWidth,
437-                 height =  newHeight,
438-                 topMargin =  newTopPadding,
439-                 leftMargin =  newLeftPadding,
440-                 visible =  true ,
441-                 adapterKey =  adapter.type,
442-                 attrs =  attrs
443-             )
444-             mutableState
571+         //  Let adapter compute/measure if it wants to
572+         val  fromAdapter =  adapter.preComposeMeasureHeight(attrs, widthPx, measurer)
573+         if  (fromAdapter !=  null  &&  fromAdapter >=  0 ) return  fromAdapter
574+         //  If adapter did not implement it but hinted wrap content policy, measure the actual content once
575+         if  (adapter.sizingPolicy(attrs) ==  ComposePlaceholderAdapter .SizingPolicy .MatchWidthWrapContentHeight ) {
576+             val  uuid =  attrs.getValue(UUID_ATTRIBUTE )
577+             val  h =  measurer.measure(content =  { adapter.Placeholder (uuid, attrs) }, widthPx =  widthPx)
578+             return  if  (h >=  0 ) h else  null 
445579        }
580+         return  null 
446581    }
447582
448583    private  fun  validateAttributes (attributes :  AztecAttributes ): Boolean  {
0 commit comments