Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,13 @@ Use the shared springs in `GlassMotion` (or springs close to them):
| `GlassMotion.unfold()` / `unfoldLag()` / `fold()` | Menus: width (0.68, 420), height a beat behind (0.6, 340), close (1, 900) |
| `GlassMotion.settle()` / `fade()` | Alpha and layout. Critically damped, never bouncy |

**The user tunes it (Settings -> Liquid Glass -> Motion).** `MotionSettings` holds multipliers on
the numbers above (bounce, speed, press depth, release jiggle, drag stretch, long-press bloom) with
the profiles Normal (the design, exactly), Performance, Bouncy and Super springy. So every bouncing
spring goes through `MotionSettings.damping(designed)` / `stiffness(designed)`, and every press
size goes through the style. The `GlassMotion` springs already do. A new bouncing spring written
by hand must do the same. `settle`, `fade` and layout springs never bounce and ignore it.

**Press jelly (the iOS 27 feel).**
- Glass buttons (`LiquidButton`, `LiquidIconButton`) share `liquidPressTransform`. The button swells
~4 dp under the finger and leans toward a dragging finger. On release it springs back and jiggles once,
Expand Down Expand Up @@ -167,6 +174,10 @@ Use the shared springs in `GlassMotion` (or springs close to them):
heading stays blue. Never use a plain RGB invert. Pictures: a photograph is drawn as printed; a
scan or figure that `NightPaper.looksLikePaper` turns over with the page. PDF/PPTX pages find
their pictures with `PdfImageRegions` and redraw photographs unfiltered (also in zoom tiles).
- The reader's bottom circles (editor tools, auto-scroll, the open/share morph) are 44 dp with
20 dp icons, the Word editor's size, in the PDF and PowerPoint viewers too. The morph grows to
twice its size while armed. Settings -> Personalization -> Swap share and open makes the tap
share and the hold-and-swipe open; the ink still follows what is behind it.
- Word documents open as pages, desktop-like: true proportions and margins, fit to width, pinch to
zoom (the page relays out sharp, it is never bitmap-scaled), Word's own page breaks.

Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,13 @@ object AppSettingsManager {
// see UISensor's own KDoc for why that is the dominant redraw cost on devices sensitive to it.
/** Documents open with dark pages whenever the app is in dark mode (#74). */
fun getAutoDarkPages(context: Context): Boolean = prefs(context).getBoolean(KEY_AUTO_DARK_PAGES, false)

/**
* Swaps the viewer's share/open button: tap shares the document, press-and-hold then swipe up
* opens another file (the default is the other way round).
*/
fun getInvertShareMorph(context: Context): Boolean = prefs(context).getBoolean("invert_share_morph", false)
fun setInvertShareMorph(context: Context, enabled: Boolean) = prefs(context).edit().putBoolean("invert_share_morph", enabled).apply()
fun setAutoDarkPages(context: Context, enabled: Boolean) = prefs(context).edit().putBoolean(KEY_AUTO_DARK_PAGES, enabled).apply()

fun getReduceGlassMotion(context: Context): Boolean =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,29 +21,29 @@ import android.graphics.PointF
* [intensity] blends the look with its input: matrices are interpolated towards identity, GPU
* looks are alpha-composited over the input.
*/
enum class FilterPreset(val label: String) {
Vivid("Vivid"),
Warm("Warm"),
Cool("Cool"),
Fade("Fade"),
Mono("Mono"),
Noir("Noir"),
Sepia("Sepia"),
Vintage("Vintage"),
Chrome("Chrome"),
Dramatic("Dramatic"),
Golden("Golden"),
Teal("Teal & Orange"),
Polaroid("Polaroid"),
Lomo("Lomo"),
Invert("Invert"),
Posterize("Posterize"),
Sketch("Sketch"),
Toon("Toon"),
Emboss("Emboss"),
Crosshatch("Crosshatch"),
Halftone("Halftone"),
OilPaint("Oil paint");
enum class FilterPreset(val label: String, @androidx.annotation.StringRes val labelRes: Int) {
Vivid("Vivid", com.chethan616.clearpdf.R.string.ie_fx_vivid),
Warm("Warm", com.chethan616.clearpdf.R.string.ie_fx_warm),
Cool("Cool", com.chethan616.clearpdf.R.string.ie_fx_cool),
Fade("Fade", com.chethan616.clearpdf.R.string.ie_fx_fade),
Mono("Mono", com.chethan616.clearpdf.R.string.ie_fx_mono),
Noir("Noir", com.chethan616.clearpdf.R.string.ie_fx_noir),
Sepia("Sepia", com.chethan616.clearpdf.R.string.ie_fx_sepia),
Vintage("Vintage", com.chethan616.clearpdf.R.string.ie_fx_vintage),
Chrome("Chrome", com.chethan616.clearpdf.R.string.ie_fx_chrome),
Dramatic("Dramatic", com.chethan616.clearpdf.R.string.ie_fx_dramatic),
Golden("Golden", com.chethan616.clearpdf.R.string.ie_fx_golden),
Teal("Teal & Orange", com.chethan616.clearpdf.R.string.ie_fx_teal),
Polaroid("Polaroid", com.chethan616.clearpdf.R.string.ie_fx_polaroid),
Lomo("Lomo", com.chethan616.clearpdf.R.string.ie_fx_lomo),
Invert("Invert", com.chethan616.clearpdf.R.string.ie_fx_invert),
Posterize("Posterize", com.chethan616.clearpdf.R.string.ie_fx_posterize),
Sketch("Sketch", com.chethan616.clearpdf.R.string.ie_fx_sketch),
Toon("Toon", com.chethan616.clearpdf.R.string.ie_fx_toon),
Emboss("Emboss", com.chethan616.clearpdf.R.string.ie_fx_emboss),
Crosshatch("Crosshatch", com.chethan616.clearpdf.R.string.ie_fx_crosshatch),
Halftone("Halftone", com.chethan616.clearpdf.R.string.ie_fx_halftone),
OilPaint("Oil paint", com.chethan616.clearpdf.R.string.ie_fx_oil_paint);

fun matrix(): ColorMatrix? = when (this) {
Vivid -> ColorMatrix().apply { setSaturation(1.45f) }.then(contrast(1.08f))
Expand Down
54 changes: 53 additions & 1 deletion app/src/main/java/com/chethan616/clearpdf/ui/DocsApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,9 @@ fun DocsApp(shortcutRoute: String? = null, incomingPdfUri: android.net.Uri? = nu
}
}
var showStarPrompt by rememberSaveable { mutableStateOf(false) }
// Switching into or out of a right-to-left language (Arabic) can't flip the layout in place;
// the app offers a restart instead.
var restartForDirection by rememberSaveable { mutableStateOf(false) }
val systemDark = isSystemInDarkTheme()
val isDarkMode = when (themeMode) {
1 -> false
Expand Down Expand Up @@ -284,7 +287,11 @@ fun DocsApp(shortcutRoute: String? = null, incomingPdfUri: android.net.Uri? = nu
// Floating glass (dropdown menus, contextual bars) from any depth renders here, above every
// screen, refracting the live screen — see GlassOverlayHost.
val overlayHost = remember { GlassOverlayHost() }
remember(context) { com.chethan616.clearpdf.ui.components.GlassSettings.load(context); true }
remember(context) {
com.chethan616.clearpdf.ui.components.GlassSettings.load(context)
com.chethan616.clearpdf.ui.components.MotionSettings.load(context)
true
}
val platformHaptics = androidx.compose.ui.platform.LocalHapticFeedback.current
val haptics = remember(platformHaptics) { com.chethan616.clearpdf.ui.utils.ThrottledHaptics(platformHaptics) }

Expand Down Expand Up @@ -363,6 +370,7 @@ fun DocsApp(shortcutRoute: String? = null, incomingPdfUri: android.net.Uri? = nu
onLocaleChanged = { code ->
val normalized = com.chethan616.clearpdf.ui.utils.LocaleHelper.normalizeForUi(code)
if (normalized != selectedLocale) {
if (isRtlTag(normalized) != isRtlTag(selectedLocale)) restartForDirection = true
// Persist, then switch IN PLACE: the hoisted locale re-provides
// LocalResources, so every stringResource re-resolves on the next frame
// (the path Onboarding uses). No fade-out + Activity recreate — that was
Expand Down Expand Up @@ -513,9 +521,53 @@ fun DocsApp(shortcutRoute: String? = null, incomingPdfUri: android.net.Uri? = nu
}
}

// A new language that reads the other way round (into or out of Arabic): strings
// switch in place, but the layout direction only flips with a fresh start.
GlassDialog(
visible = restartForDirection,
onDismiss = { restartForDirection = false },
backdrop = contentBackdrop,
title = stringResource(R.string.restart_title),
actions = {
GlassDialogAction(stringResource(R.string.restart_later), onClick = { restartForDirection = false })
GlassDialogAction(
stringResource(R.string.restart_now),
onClick = { restartForDirection = false; restartApp(context) },
primary = true
)
}
) {
BasicText(
stringResource(R.string.restart_body),
style = TextStyle(
if (!isDarkMode) Color(0xFF3A3A3C) else Color(0xFFD1D1D6),
15.sp,
textAlign = TextAlign.Center
),
modifier = Modifier.fillMaxWidth()
)
}

// Last child: above the tab bar and dialogs, outside the layer it samples.
GlassOverlayLayer(overlayHost, contentBackdrop)
}
}
}
}

/** Whether [tag]'s language reads right to left (Arabic, in the app's languages). */
private fun isRtlTag(tag: String): Boolean =
android.text.TextUtils.getLayoutDirectionFromLocale(java.util.Locale.forLanguageTag(tag)) == android.view.View.LAYOUT_DIRECTION_RTL

/**
* Starts the app afresh, so a language that reads the other way round lays every screen out in its
* direction (attachBaseContext applies the saved language on the way up).
*/
private fun restartApp(context: android.content.Context) {
val intent = context.packageManager.getLaunchIntentForPackage(context.packageName)
?.addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK or android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK)
?: return
context.startActivity(intent)
context.findActivity()?.finishAffinity()
Runtime.getRuntime().exit(0)
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ fun GlassBead(
selected -> 1.1f
else -> 1f
},
spring(dampingRatio = 0.45f, stiffness = 520f),
spring(dampingRatio = MotionSettings.damping(0.45f), stiffness = MotionSettings.stiffness(520f)),
label = "beadScale"
)
Box(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ fun GlassBottomSheet(
hidden.snapTo(startFraction)
currentOnDismiss()
} else {
dragOffset.animateTo(0f, spring(dampingRatio = 0.7f, stiffness = 500f), initialVelocity = velocity)
dragOffset.animateTo(0f, spring(dampingRatio = MotionSettings.damping(0.7f), stiffness = MotionSettings.stiffness(500f)), initialVelocity = velocity)
}
}
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ fun GlassColorPicker(
themeColors: List<Color> = OfficeThemeColors,
showAlpha: Boolean = false,
onClear: (() -> Unit)? = null,
clearLabel: String = "No colour",
clearLabel: String? = null,
standardColors: List<Color> = OfficeStandardColors,
/**
* Optional: the 5 tint/shade variants under each theme swatch. Defaults to [officeVariants];
Expand Down Expand Up @@ -238,11 +238,11 @@ fun GlassColorPicker(
horizontalArrangement = Arrangement.spacedBy(10.dp)
) {
Icon(Icons.Rounded.Block, contentDescription = null, tint = secondary, modifier = Modifier.size(22.dp))
BasicText(clearLabel, style = TextStyle(textColor, 15.sp, FontWeight.Medium))
BasicText(clearLabel ?: androidx.compose.ui.res.stringResource(com.chethan616.clearpdf.R.string.color_none), style = TextStyle(textColor, 15.sp, FontWeight.Medium))
}
}

SectionLabel("Theme colours", secondary)
SectionLabel(androidx.compose.ui.res.stringResource(com.chethan616.clearpdf.R.string.color_theme), secondary)
// Theme grid: base row, gap, then 5 variant rows.
val columns = themeColors.map { listOf(it) + themeVariants(it) }
fun pickTheme(col: Int, row: Int) {
Expand All @@ -262,11 +262,11 @@ fun GlassColorPicker(
}
}

SectionLabel("Standard colours", secondary)
SectionLabel(androidx.compose.ui.res.stringResource(com.chethan616.clearpdf.R.string.color_standard), secondary)
SwatchRow(standardColors, color, isDark) { pick(it) }

if (recent.isNotEmpty()) {
SectionLabel("Recent colours", secondary)
SectionLabel(androidx.compose.ui.res.stringResource(com.chethan616.clearpdf.R.string.color_recent), secondary)
SwatchRow(
recent.toList() + List((themeColors.size - recent.size).coerceAtLeast(0)) { Color.Unspecified },
color, isDark
Expand All @@ -293,7 +293,7 @@ fun GlassColorPicker(
)
)
)
BasicText("More colours…", Modifier.weight(1f), style = TextStyle(textColor, 15.sp, FontWeight.Medium))
BasicText(androidx.compose.ui.res.stringResource(com.chethan616.clearpdf.R.string.color_more), Modifier.weight(1f), style = TextStyle(textColor, 15.sp, FontWeight.Medium))
Icon(
if (customOpen) Icons.Rounded.ExpandLess else Icons.Rounded.ExpandMore,
contentDescription = null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ fun GlassDialog(
LaunchedEffect(visible) {
if (visible) {
composed = true
progress.animateTo(1f, spring(dampingRatio = 0.72f, stiffness = 420f, visibilityThreshold = 0.001f))
progress.animateTo(1f, spring(dampingRatio = MotionSettings.damping(0.72f), stiffness = MotionSettings.stiffness(420f), visibilityThreshold = 0.001f))
} else {
progress.animateTo(0f, spring(dampingRatio = 1f, stiffness = 900f, visibilityThreshold = 0.001f))
composed = false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,17 @@ import androidx.compose.animation.core.spring
* the press reads instantly), lifts on [pressOut] (springs once past rest, then settles), and glass
* controls add a [wobble] (X and Y squash out of phase, one visible swing). A tap shorter than
* [MinPressMillis] still shows the full press before it springs back.
*
* Every bouncing spring here goes through [MotionSettings] (Settings -> Liquid Glass -> Motion), so
* the user's bounce and speed apply everywhere; [settle], [fade] and [fold] never bounce.
*/
object GlassMotion {

/** Underdamped: overshoots ~8% and settles. The capsule/bar "springs open" feel. */
fun <T> morph(): SpringSpec<T> = spring(dampingRatio = 0.58f, stiffness = 420f)
fun <T> morph(): SpringSpec<T> = spring(MotionSettings.damping(0.58f), MotionSettings.stiffness(420f))

/** Snappier and bouncier — for small elements that should pop, like an icon landing. */
fun <T> pop(): SpringSpec<T> = spring(dampingRatio = 0.45f, stiffness = 500f)
fun <T> pop(): SpringSpec<T> = spring(MotionSettings.damping(0.45f), MotionSettings.stiffness(500f))

/** Critically damped. For alpha, and for layout properties where overshoot costs a re-measure. */
fun <T> settle(): SpringSpec<T> = spring(dampingRatio = 1f, stiffness = Spring.StiffnessMedium)
Expand All @@ -35,28 +38,28 @@ object GlassMotion {
fun <T> fade(): SpringSpec<T> = settle()

/** Press-down and bounce-back for value-driven presses (prefer [rememberJellyPress]). */
fun <T> press(): SpringSpec<T> = spring(dampingRatio = 0.5f, stiffness = 700f)
fun <T> press(): SpringSpec<T> = spring(MotionSettings.damping(0.5f), MotionSettings.stiffness(700f))

/** Finger down: quick and nearly critically damped, so the press lands in ~80 ms. */
fun <T> pressIn(): SpringSpec<T> = spring(dampingRatio = 0.72f, stiffness = 900f)
fun <T> pressIn(): SpringSpec<T> = spring(MotionSettings.damping(0.72f), MotionSettings.stiffness(900f))

/** Finger up: springs back past rest once (~2 % of the dip) and settles. */
fun <T> pressOut(): SpringSpec<T> = spring(dampingRatio = 0.46f, stiffness = 420f)
fun <T> pressOut(): SpringSpec<T> = spring(MotionSettings.damping(0.46f), MotionSettings.stiffness(420f))

/** Release jiggle for glass: one visible out-of-phase squash, the second swing is ~20 %. */
fun <T> wobble(): SpringSpec<T> = spring(dampingRatio = 0.42f, stiffness = 420f)
fun <T> wobble(): SpringSpec<T> = spring(MotionSettings.damping(0.42f), MotionSettings.stiffness(420f))

/** Menus and popovers unfolding from their anchor: ~5 % overshoot, settled in ~350 ms. */
fun <T> unfold(): SpringSpec<T> = spring(dampingRatio = 0.68f, stiffness = 420f)
fun <T> unfold(): SpringSpec<T> = spring(MotionSettings.damping(0.68f), MotionSettings.stiffness(420f))

/** The trailing axis of an unfold: a touch softer, so the panel lands like a drop of gel. */
fun <T> unfoldLag(): SpringSpec<T> = spring(dampingRatio = 0.6f, stiffness = 340f)
fun <T> unfoldLag(): SpringSpec<T> = spring(MotionSettings.damping(0.6f), MotionSettings.stiffness(340f))

/** Folding away: fast and critically damped. Dismissals never bounce. */
fun <T> fold(): SpringSpec<T> = spring(dampingRatio = 1f, stiffness = 900f)
fun <T> fold(): SpringSpec<T> = spring(dampingRatio = 1f, stiffness = MotionSettings.stiffness(900f))

/** Uniform pressed scale for tiles, rows and morphing buttons. */
const val PressedScale = 0.94f
/** Uniform pressed scale for tiles, rows and morphing buttons (0.94 at the designed press depth). */
val PressedScale: Float get() = 1f - 0.06f * MotionSettings.style.press.coerceIn(0f, 2.5f)

/** A tap shorter than this still shows the whole press before releasing. */
const val MinPressMillis = 90L
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ fun GlassSegmentedControl(
hoverIndex = selectedIndex
position.animateTo(
selectedIndex.toFloat().fastCoerceIn(0f, (count - 1).toFloat()),
spring(dampingRatio = 0.72f, stiffness = 380f)
spring(dampingRatio = MotionSettings.damping(0.72f), stiffness = MotionSettings.stiffness(380f))
) { stretch.onPositionSample(value * segPx) }
}

Expand Down Expand Up @@ -160,7 +160,7 @@ fun GlassSegmentedControl(
}
}
}
scope.launch { press.animateTo(0f, spring(0.6f, 400f)) }
scope.launch { press.animateTo(0f, spring(MotionSettings.damping(0.6f), MotionSettings.stiffness(400f))) }
val target = if (dragged) hoverIndex else startIndex
if (!cancelled && target != currentSelected) {
haptics.performHapticFeedback(HapticFeedbackType.SegmentTick)
Expand All @@ -169,7 +169,7 @@ fun GlassSegmentedControl(
// Snap the thumb back if a drag ended on the same segment.
hoverIndex = currentSelected
scope.launch {
position.animateTo(currentSelected.toFloat(), spring(0.72f, 380f)) {
position.animateTo(currentSelected.toFloat(), spring(MotionSettings.damping(0.72f), MotionSettings.stiffness(380f))) {
stretch.onPositionSample(value * segPx)
}
}
Expand Down
Loading
Loading