From 9a4131ee3dd326d9a8db243d8ab0959e188cdab5 Mon Sep 17 00:00:00 2001 From: sergeych Date: Sat, 22 Nov 2025 14:21:45 +0100 Subject: [PATCH] lyngweb: refactor `SiteHighlight` to align package structure and add adjustable textarea height --- .../kotlin/net/sergeych/lyngweb/Editor.kt | 58 ++++++++++++++++++- .../{site => lyngweb}/SiteHighlight.kt | 27 +-------- site/src/jsMain/kotlin/TryLyngPage.kt | 4 +- site/src/jsTest/kotlin/HighlightSmokeTest.kt | 1 + 4 files changed, 62 insertions(+), 28 deletions(-) rename lyngweb/src/jsMain/kotlin/net/sergeych/{site => lyngweb}/SiteHighlight.kt (73%) diff --git a/lyngweb/src/jsMain/kotlin/net/sergeych/lyngweb/Editor.kt b/lyngweb/src/jsMain/kotlin/net/sergeych/lyngweb/Editor.kt index 8f266a8..a7594f1 100644 --- a/lyngweb/src/jsMain/kotlin/net/sergeych/lyngweb/Editor.kt +++ b/lyngweb/src/jsMain/kotlin/net/sergeych/lyngweb/Editor.kt @@ -19,7 +19,6 @@ package net.sergeych.lyngweb import androidx.compose.runtime.* import kotlinx.browser.window -import net.sergeych.site.SiteHighlight import org.jetbrains.compose.web.attributes.placeholder import org.jetbrains.compose.web.dom.Div import org.jetbrains.compose.web.events.SyntheticKeyboardEvent @@ -48,6 +47,10 @@ fun EditorWithOverlay( setCode: (String) -> Unit, tabSize: Int = 4, onKeyDown: ((SyntheticKeyboardEvent) -> Unit)? = null, + // New sizing controls + minRows: Int = 6, + maxRows: Int? = null, + autoGrow: Boolean = false, ) { var overlayEl by remember { mutableStateOf(null) } var taEl by remember { mutableStateOf(null) } @@ -57,6 +60,47 @@ fun EditorWithOverlay( var pendingSelEnd by remember { mutableStateOf(null) } var pendingScrollTop by remember { mutableStateOf(null) } var pendingScrollLeft by remember { mutableStateOf(null) } + var cachedLineHeight by remember { mutableStateOf(null) } + var cachedVInsets by remember { mutableStateOf(null) } + + fun ensureMetrics(ta: HTMLTextAreaElement) { + if (cachedLineHeight == null || cachedVInsets == null) { + val cs = window.getComputedStyle(ta) + val lhStr = cs.getPropertyValue("line-height").trim() + val lh = lhStr.removeSuffix("px").toDoubleOrNull() ?: 20.0 + fun parsePx(name: String): Double { + val v = cs.getPropertyValue(name).trim().removeSuffix("px").toDoubleOrNull() + return v ?: 0.0 + } + val pt = parsePx("padding-top") + val pb = parsePx("padding-bottom") + val bt = parsePx("border-top-width") + val bb = parsePx("border-bottom-width") + cachedLineHeight = lh + cachedVInsets = pt + pb + bt + bb + } + } + + fun rowsToPx(rows: Int): Double? { + val lh = cachedLineHeight ?: return null + val ins = cachedVInsets ?: 0.0 + return lh * rows + ins + } + + fun adjustTextareaHeight() { + val ta = taEl ?: return + if (!autoGrow) return + ensureMetrics(ta) + // reset to auto to measure full scrollHeight + ta.style.height = "auto" + val minPx = rowsToPx(minRows) + val maxPx = maxRows?.let { rowsToPx(it) } + var target = ta.scrollHeight.toDouble() + if (minPx != null && target < minPx) target = minPx + if (maxPx != null && target > maxPx) target = maxPx + // Apply target height + ta.style.height = "${target}px" + } // Update overlay HTML whenever code changes LaunchedEffect(code) { @@ -139,6 +183,8 @@ fun EditorWithOverlay( overlayEl?.scrollLeft = sl pendingScrollTop = null pendingScrollLeft = null + // If text changed and autoGrow enabled, adjust height + adjustTextareaHeight() } fun setSelection(start: Int, end: Int = start) { @@ -177,6 +223,9 @@ fun EditorWithOverlay( org.jetbrains.compose.web.dom.TextArea(value = code, attrs = { ref { ta -> taEl = ta + // Cache metrics and adjust size on first mount + ensureMetrics(ta) + adjustTextareaHeight() onDispose { if (taEl === ta) taEl = null } } // Avoid relying on external classes; still allow host app to override via CSS @@ -184,7 +233,7 @@ fun EditorWithOverlay( attr( "style", buildString { - append("width:100%; min-height:220px; background:transparent; position:relative; z-index:1; tab-size:") + append("width:100%; background:transparent; position:relative; z-index:1; tab-size:") append(tabSize) append("; color:transparent; -webkit-text-fill-color:transparent; ") // Make caret visible even though text color is transparent @@ -196,6 +245,8 @@ fun EditorWithOverlay( append(" outline: none; box-shadow: none;") // Typography and rendering append(" font-variant-ligatures: none; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace;") + // Keep previous visual minimum for TryLyng unless overridden by rows logic + append(" min-height:220px;") } ) // Disable browser corrections for a code editor @@ -203,11 +254,14 @@ fun EditorWithOverlay( attr("autocorrect", "off") attr("autocapitalize", "off") attr("autocomplete", "off") + // Provide a baseline number of rows for browsers that use it + attr("rows", minRows.toString()) placeholder("Enter Lyng code here…") onInput { ev -> val v = (ev.target as HTMLTextAreaElement).value setCode(v) + adjustTextareaHeight() } onKeyDown { ev -> diff --git a/lyngweb/src/jsMain/kotlin/net/sergeych/site/SiteHighlight.kt b/lyngweb/src/jsMain/kotlin/net/sergeych/lyngweb/SiteHighlight.kt similarity index 73% rename from lyngweb/src/jsMain/kotlin/net/sergeych/site/SiteHighlight.kt rename to lyngweb/src/jsMain/kotlin/net/sergeych/lyngweb/SiteHighlight.kt index 6c541c5..b728d99 100644 --- a/lyngweb/src/jsMain/kotlin/net/sergeych/site/SiteHighlight.kt +++ b/lyngweb/src/jsMain/kotlin/net/sergeych/lyngweb/SiteHighlight.kt @@ -1,30 +1,7 @@ -/* - * Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -/* - * Site-like wrapper for Lyng highlighter that renders HTML spans with CSS classes - * compatible with the site styles. Kept in package net.sergeych.site for backwards - * compatibility with existing code and tests. - */ -package net.sergeych.site +package net.sergeych.lyngweb import net.sergeych.lyng.highlight.HighlightKind import net.sergeych.lyng.highlight.SimpleLyngHighlighter -import net.sergeych.lyngweb.htmlEscape /** * Minimal HTML renderer for Lyng syntax highlighting, compatible with the site CSS. @@ -84,4 +61,4 @@ object SiteHighlight { if (pos < text.length) sb.append(htmlEscape(text.substring(pos))) return sb.toString() } -} +} \ No newline at end of file diff --git a/site/src/jsMain/kotlin/TryLyngPage.kt b/site/src/jsMain/kotlin/TryLyngPage.kt index daf1249..f467017 100644 --- a/site/src/jsMain/kotlin/TryLyngPage.kt +++ b/site/src/jsMain/kotlin/TryLyngPage.kt @@ -145,7 +145,9 @@ fun TryLyngPage() { ev.preventDefault() runCode() } - } + }, + // Keep current initial size but allow the editor to grow with content + autoGrow = true ) } diff --git a/site/src/jsTest/kotlin/HighlightSmokeTest.kt b/site/src/jsTest/kotlin/HighlightSmokeTest.kt index b66307a..3f56a3a 100644 --- a/site/src/jsTest/kotlin/HighlightSmokeTest.kt +++ b/site/src/jsTest/kotlin/HighlightSmokeTest.kt @@ -20,6 +20,7 @@ package net.sergeych.site import net.sergeych.lyng.highlight.HighlightKind import net.sergeych.lyng.highlight.HighlightSpan import net.sergeych.lyng.highlight.SimpleLyngHighlighter +import net.sergeych.lyngweb.SiteHighlight import kotlin.test.Test import kotlin.test.assertContains import kotlin.test.assertTrue