Skip to content
50 changes: 37 additions & 13 deletions compose-rust/android-kotlin/shared/HostButton.kt
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import androidx.compose.ui.graphics.ColorProducer
import androidx.compose.ui.layout.layout
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.selected
Expand All @@ -48,6 +49,7 @@ import dev.darkpyonix.composerust.protocol.TextAlign
import dev.darkpyonix.composerust.design.compositeOver
import dev.darkpyonix.composerust.design.ensureContrast
import dev.darkpyonix.composerust.protocol.Modifier as ProtocolModifier
import dev.darkpyonix.composerust.ui.resolvedShape
import dev.darkpyonix.composerust.design.ButtonStyle
import dev.darkpyonix.composerust.design.ResolvedTheme
import dev.darkpyonix.composerust.runtime.EventDispatcher
Expand Down Expand Up @@ -130,6 +132,29 @@ internal fun HostButton(
val available = if (enabled) Modifier else Modifier.groupAlpha { style.disabledAlpha }
val overlayFocusStack = LocalOverlayFocusStack.current
val focusRequester = remember(node.id) { FocusRequester() }
val ownShape = node.modifiers.any {
it is ProtocolModifier.Shape ||
it is ProtocolModifier.ShapeRole ||
it is ProtocolModifier.CornerEach
}
val shape = if (ownShape) node.modifiers.resolvedShape(theme) else style.shape
val hasExplicitSize = node.modifiers.any {
it is ProtocolModifier.Size ||
it is ProtocolModifier.Width ||
it is ProtocolModifier.Height ||
it is ProtocolModifier.RequiredSize
}
val setsOwnHeight = node.modifiers.any {
it is ProtocolModifier.Size ||
it is ProtocolModifier.Height ||
it is ProtocolModifier.RequiredSize
}
// Read before the decoration, because how much room the button needs depends on it.
val wordless = node.text(PropertyKind.Text).isEmpty() &&
node.role(PropertyKind.Icon, IconRole.entries.toTypedArray()) != null
val labelPadH = if (wordless && hasExplicitSize) 0.dp else if (wordless) style.verticalPadding else style.horizontalPadding
val labelPadV = if (wordless && hasExplicitSize) 0.dp else style.verticalPadding

val clickable = if (handlerId == null) {
available.then(modifier)
} else {
Expand All @@ -140,7 +165,7 @@ internal fun HostButton(
.focusRequester(focusRequester)
.onFocusChanged { state -> overlayFocusStack.noteFocused(focusRequester, state.isFocused) }
.pressFeedback(
interactions, enabled, theme, style.shape,
interactions, enabled, theme, shape,
node.paintProp(PropertyKind.Color)?.let { theme.color(it) } ?: style.content,
node.modifiers.lastOrNull { it is dev.darkpyonix.composerust.protocol.Modifier.PressFeedback } as? dev.darkpyonix.composerust.protocol.Modifier.PressFeedback,
).clickable(
Expand All @@ -160,30 +185,29 @@ internal fun HostButton(
// pill, and asking for a variant instead hands the question to whichever design
// system is active, which answers with its own accent.
val ownFill = node.setsOwnBackground()
// Read before the decoration, because how much room the button needs depends on it.
val wordless = node.text(PropertyKind.Text).isEmpty() &&
node.role(PropertyKind.Icon, IconRole.entries.toTypedArray()) != null
val decorated = theme.rules
.elevation(clickable, elevation, style.shape, theme)
.clip(style.shape)
.then(if (ownFill) Modifier else Modifier.background(container, style.shape))
.elevation(clickable, elevation, shape, theme)
.clip(shape)
.then(if (ownFill) Modifier else Modifier.background(container, shape))
.then(
if (style.borderWidth.value > 0f) {
Modifier.border(style.borderWidth, borderColor, style.shape)
Modifier.border(style.borderWidth, borderColor, shape)
} else {
Modifier
},
)
.topHighlight(style.topHighlight)
.defaultMinSize(minHeight = style.minHeight)
.let {
if (setsOwnHeight || (wordless && hasExplicitSize)) it else it.defaultMinSize(minHeight = style.minHeight)
}
// A button with a glyph and no word gets the same room on both axes. The
// horizontal padding is there to keep a word off the button's ends, and a button
// with no word has nothing to keep off them: using it anyway makes every icon
// control half as wide again as it needs to be, which two of them in a floating
// group and one at the head of a composer both showed.
.labelPadding(
horizontal = if (wordless) style.verticalPadding else style.horizontalPadding,
vertical = style.verticalPadding,
horizontal = labelPadH,
vertical = labelPadV,
alignStart = alignStart,
)

Expand Down Expand Up @@ -266,8 +290,8 @@ internal fun HostButton(
}
Box(
Modifier.matchParentSize().labelPadding(
horizontal = if (wordless) style.verticalPadding else style.horizontalPadding,
vertical = style.verticalPadding,
horizontal = labelPadH,
vertical = labelPadV,
alignStart = alignStart,
),
contentAlignment = alignment,
Expand Down
40 changes: 28 additions & 12 deletions compose-rust/android-kotlin/shared/HostTextField.kt
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ import dev.darkpyonix.composerust.ui.textStyle
import dev.darkpyonix.composerust.ui.resolvedShape
import dev.darkpyonix.composerust.ui.node.HostText
import dev.darkpyonix.composerust.design.LocalGlassDepth
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.ui.unit.Dp
import dev.darkpyonix.composerust.design.ResolvedTheme
import dev.darkpyonix.composerust.protocol.Modifier as ProtocolModifier

/**
* The uncontrolled TextField.
Expand Down Expand Up @@ -272,6 +276,12 @@ internal fun HostTextField(
it is dev.darkpyonix.composerust.protocol.Modifier.Height ||
it is dev.darkpyonix.composerust.protocol.Modifier.Size
}
val setsOwnHeight = node.modifiers.any {
it is dev.darkpyonix.composerust.protocol.Modifier.FillMaxHeight ||
it is dev.darkpyonix.composerust.protocol.Modifier.Height ||
it is dev.darkpyonix.composerust.protocol.Modifier.Size ||
it is dev.darkpyonix.composerust.protocol.Modifier.RequiredSize
}

// What the node named for itself replaces what the design system would have drawn
// The system's frame is the default for a field that said nothing, and a field
Expand All @@ -287,11 +297,6 @@ internal fun HostTextField(
it is dev.darkpyonix.composerust.protocol.Modifier.ShapeRole ||
it is dev.darkpyonix.composerust.protocol.Modifier.CornerEach
}
val ownRoom = node.modifiers.any {
it is dev.darkpyonix.composerust.protocol.Modifier.Padding ||
it is dev.darkpyonix.composerust.protocol.Modifier.PaddingEach ||
it is dev.darkpyonix.composerust.protocol.Modifier.PaddingRole
}
val frameShape = if (ownShape) node.modifiers.resolvedShape(theme) else frame.shape

BasicTextField(
Expand All @@ -312,19 +317,14 @@ internal fun HostTextField(
// could only put the caret in by hitting the words.
.fillMaxWidth()
.let { if (fillsHeight) it.fillMaxHeight() else it }
.defaultMinSize(minHeight = frame.minHeight)
.let { if (setsOwnHeight) it else it.defaultMinSize(minHeight = frame.minHeight) }
.clip(frameShape)
.let {
if (ownFill) it else it.background(if (focused) frame.containerFocused else frame.container)
}
.let { if (ownLine) it else it.fieldBorder(frame.copy(shape = frameShape), focused) }
.let { if (ownLine || ownFill) it else it.fieldUnderline(frame.underline, focused) }
.let {
if (ownRoom) it else it.padding(
horizontal = frame.horizontalPadding,
vertical = frame.verticalPadding,
)
},
.padding(node.contentPadding(theme, frame.horizontalPadding, frame.verticalPadding)),
contentAlignment = if (multiline) Alignment.TopStart else Alignment.CenterStart,
) {
if (value.text.isEmpty() && placeholder.isNotEmpty()) {
Expand Down Expand Up @@ -436,3 +436,19 @@ private class FontChainTransformation(private val chain: FontChain) : VisualTran

override fun hashCode(): Int = chain.hashCode()
}

internal fun Node.contentPadding(
theme: ResolvedTheme,
defaultHorizontal: Dp,
defaultVertical: Dp,
): PaddingValues {
val padEach = modifiers.filterIsInstance<ProtocolModifier.PaddingEach>().lastOrNull()
if (padEach != null) {
return PaddingValues(padEach.start.dp, padEach.top.dp, padEach.end.dp, padEach.bottom.dp)
}
val pad = modifiers.filterIsInstance<ProtocolModifier.Padding>().lastOrNull()
if (pad != null) return PaddingValues(pad.value.dp)
val padRole = modifiers.filterIsInstance<ProtocolModifier.PaddingRole>().lastOrNull()
if (padRole != null) return PaddingValues(theme.space(padRole.role))
return PaddingValues(horizontal = defaultHorizontal, vertical = defaultVertical)
}
16 changes: 13 additions & 3 deletions compose-rust/android-kotlin/shared/ModifierChain.kt
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ internal fun List<ProtocolModifier>.toComposeModifier(
containerShape: Shape? = null,
pressColor: Color = theme.color(ColorRole.OnSurface),
allowPress: Boolean = true,
allowPadding: Boolean = true,
): Modifier {
// The last Shape or ShapeRole in the list is what clips, what the border
// follows and what the background fills, whatever their order in the chain.
Expand Down Expand Up @@ -192,7 +193,8 @@ internal fun List<ProtocolModifier>.toComposeModifier(
// Applied with the node's accessible name in one place, `accessibility`.
is ProtocolModifier.Semantics -> chain
is ProtocolModifier.Empty -> chain
is ProtocolModifier.Padding -> attachPressableIfNeeded(chain).padding(value.value.dp)
is ProtocolModifier.Padding ->
if (allowPadding) attachPressableIfNeeded(chain).padding(value.value.dp) else chain
is ProtocolModifier.MatchParentSize -> chain
is ProtocolModifier.FillMaxWidth -> chain.fillMaxWidth()
is ProtocolModifier.FillMaxHeight -> chain.fillMaxHeight()
Expand All @@ -213,9 +215,17 @@ internal fun List<ProtocolModifier>.toComposeModifier(
}

is ProtocolModifier.PaddingEach ->
attachPressableIfNeeded(chain).padding(value.start.dp, value.top.dp, value.end.dp, value.bottom.dp)
if (allowPadding) {
attachPressableIfNeeded(chain).padding(value.start.dp, value.top.dp, value.end.dp, value.bottom.dp)
} else {
chain
}
is ProtocolModifier.PaddingRole ->
attachPressableIfNeeded(chain).padding(theme.space(value.role))
if (allowPadding) {
attachPressableIfNeeded(chain).padding(theme.space(value.role))
} else {
chain
}
is ProtocolModifier.Shape -> chain.clip(shape)
is ProtocolModifier.ShapeRole -> chain.clip(shape)
is ProtocolModifier.Border -> theme.brush(value.paint)?.let { brush ->
Expand Down
1 change: 1 addition & 0 deletions compose-rust/android-kotlin/shared/RenderNode.kt
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ fun RenderNode(
containerShape = if (clickHandler != null) node.pressedShape(theme) else null,
pressColor = node.paintProp(PropertyKind.Color)?.let { theme.color(it) } ?: theme.color(ColorRole.OnSurface),
allowPress = node.widget != WidgetKind.TextField,
allowPadding = node.widget != WidgetKind.TextField,
),
)
.testTag(nodeTestTag(nodeId))
Expand Down
58 changes: 58 additions & 0 deletions compose-rust/src/ui/widgets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -949,6 +949,11 @@ pub struct TextFieldScope {
enabled: bool,
multiline: bool,
type_role: Option<TypeRole>,
font_size: Option<f32>,
font_weight: Option<u16>,
line_height: Option<f32>,
letter_spacing: Option<f32>,
color: Option<Paint>,
font: FontRefs,
on_value_change: Option<Box<dyn FnMut(String)>>,
on_submit: Option<Box<dyn FnMut(String)>>,
Expand All @@ -971,6 +976,11 @@ pub fn TextField() -> TextFieldScope {
enabled: true,
multiline: false,
type_role: None,
font_size: None,
font_weight: None,
line_height: None,
letter_spacing: None,
color: None,
font: FontRefs::default(),
on_value_change: None,
on_submit: None,
Expand Down Expand Up @@ -1014,6 +1024,36 @@ impl TextFieldScope {
self
}

/// Explicit font size in sp. Overrides the type role's size.
pub fn font_size(mut self, sp: f32) -> Self {
self.font_size = Some(sp);
self
}

/// Explicit font weight (e.g. 400, 500, 600, 700). Overrides the type role's weight.
pub fn font_weight(mut self, weight: u16) -> Self {
self.font_weight = Some(weight);
self
}

/// Explicit line height in sp.
pub fn line_height(mut self, sp: f32) -> Self {
self.line_height = Some(sp);
self
}

/// Explicit letter spacing in sp.
pub fn letter_spacing(mut self, sp: f32) -> Self {
self.letter_spacing = Some(sp);
self
}

/// Explicit text colour.
pub fn color(mut self, paint: Paint) -> Self {
self.color = Some(paint);
self
}

/// The fonts to try for the field's text, in order. Taken only with `TypeRole::None`.
pub fn font(mut self, font: FontRefs) -> Self {
self.font = font;
Expand Down Expand Up @@ -1064,6 +1104,9 @@ impl TextFieldScope {
let text = std::mem::take(&mut self.text);
let placeholder = std::mem::take(&mut self.placeholder);
let (enabled, multiline, type_role) = (self.enabled, self.multiline, self.type_role);
let (font_size, font_weight, line_height) =
(self.font_size, self.font_weight, self.line_height);
let (letter_spacing, color) = (self.letter_spacing, self.color);
let font = std::mem::take(&mut self.font);
let key_down: EventCallback = match self.on_key_down.take() {
Some(handler) => EventCallback::Key(Rc::new(RefCell::new(handler))),
Expand Down Expand Up @@ -1106,6 +1149,21 @@ impl TextFieldScope {
attrs.push(("enabled", OwnedAttr::Bool(enabled)));
attrs.push(("multiline", OwnedAttr::Bool(multiline)));
attrs.push(("type_role", role_or_zero(type_role)));
if let Some(font_size) = font_size {
attrs.push(("font_size", OwnedAttr::Float(font_size as f64)));
}
if let Some(font_weight) = font_weight {
attrs.push(("font_weight", OwnedAttr::Int(font_weight as i64)));
}
if let Some(line_height) = line_height {
attrs.push(("line_height", OwnedAttr::Float(line_height as f64)));
}
if let Some(letter_spacing) = letter_spacing {
attrs.push(("letter_spacing", OwnedAttr::Float(letter_spacing as f64)));
}
if let Some(color) = color {
attrs.push(("color", super::paint(Some(color))));
}
attrs.push(("font", font_bytes(&font)));
},
listeners,
Expand Down
Loading
Loading