From 37f4bcd2d57c6cae18f630ae044273dc3f8a6344 Mon Sep 17 00:00:00 2001 From: 125hz <133181124+125hz@users.noreply.github.com> Date: Tue, 3 Nov 2026 18:40:30 -0600 Subject: [PATCH] Touch controls: the Opacity slider fades the glass; a Solid button style MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Opacity slider (the in-game menu, a game's Control opacity) seemed to do nothing on iOS 26. Every control sits in one GlassEffectContainer, which renders the glass of all its children as one layer over them, so a button's own .opacity faded its label and left the glass as it was. The opacity now goes on the container (and on the group before 26), so the whole set fades. Button style, in the in-game menu under Opacity and Size and in Settings › Appearance: Glass is the system's Liquid Glass, as before; Solid draws each control as a plain opaque disc, the A/B/X/Y colours as solid fills, no glass. The Opacity slider still fades them. The choice is remembered for every game; the overlay's top bar stays glass. Co-Authored-By: Claude Fable 5.1 --- app/Madeira/ContentView.swift | 45 +++++++++++++++++++++++++-------- app/Madeira/Library.swift | 7 +++++ app/Madeira/SettingsPages.swift | 8 ++++++ docs/LIBRARY.md | 4 ++- tests/host/check-frontend.py | 13 ++++++++++ 5 files changed, 65 insertions(+), 12 deletions(-) diff --git a/app/Madeira/ContentView.swift b/app/Madeira/ContentView.swift index 43cc3cf4df..0c8531537d 100644 --- a/app/Madeira/ContentView.swift +++ b/app/Madeira/ContentView.swift @@ -4369,18 +4369,23 @@ struct TouchControlsOverlay: View { /// neighbours sit further apart than that, so nothing merges until it is /// moved almost touching. Before 26 the same views stack as plain material. @ViewBuilder private func controls(_ screen: CGSize, session: Bool) -> some View { + // A library session's Control opacity; full while editing. The opacity + // goes on the whole group, not on each button: inside a + // GlassEffectContainer the glass is rendered by the container as one + // layer over its children, so a button's own .opacity faded its label and + // left the glass as it was (the Opacity slider seemed to do nothing). + let opacity = session && !m.editing ? library.opacity : 1 let buttons = ForEach(m.controls) { c in TouchControlButton(control: c, screen: screen) - // A library session's Control opacity; full while editing. - .opacity(session && !m.editing ? library.opacity : 1) } if #available(iOS 26.0, *) { GlassEffectContainer(spacing: 12) { ZStack { buttons } .frame(width: screen.width, height: screen.height, alignment: .topLeading) } + .opacity(opacity) } else { - buttons + buttons.opacity(opacity) } } @@ -4520,6 +4525,9 @@ struct GlassShape: View { var capsule = false var cornerRadius: CGFloat = 18 var tint: Color? = nil + /// The touch controls' Solid button style (TouchControlStyle): a plain opaque + /// disc in place of the glass, the tint as its colour. + var solid = false fileprivate var shape: AnyShape { if circle { return AnyShape(Circle()) } if capsule { return AnyShape(Capsule()) } @@ -4537,7 +4545,9 @@ extension View { /// sibling of its glass ends up blurred underneath it; a label that is the /// glass view's own content is drawn on top, as the system's buttons are. @ViewBuilder func glassFace(_ g: GlassShape) -> some View { - if #available(iOS 26.0, *) { + if g.solid { + self.background(g.shape.fill(g.tint ?? TouchControlStyle.solidFill)) + } else if #available(iOS 26.0, *) { if let tint = g.tint { self.glassEffect(.regular.tint(tint.opacity(0.55)), in: g.shape) } else { @@ -4552,6 +4562,17 @@ extension View { } } +/// The touch controls' button style, remembered for every game: "glass" (the +/// system's Liquid Glass, as before) or "solid" (an opaque disc, no glass; +/// the Opacity slider still fades the whole set). Chosen in the in-game menu +/// and in Settings › Appearance. The overlay's top bar stays glass. +enum TouchControlStyle { + static let key = "madeiraControlsStyle" + static let styles: [(id: String, name: String)] = [("glass", "Glass"), ("solid", "Solid")] + static let solidFill = Color(red: 0.17, green: 0.17, blue: 0.19) + static func isSolid(_ style: String) -> Bool { style == "solid" } +} + extension ControlAction { /// Xbox face colours. Everything else is uncoloured: a wash of tint on every /// button would make the four that mean something unreadable. @@ -4570,11 +4591,13 @@ struct TouchControlButton: View { let control: TouchControl let screen: CGSize @ObservedObject private var m = TouchControlsModel.shared + @AppStorage(TouchControlStyle.key) private var style = "glass" @State private var isDown = false @State private var dragBase: CGPoint? @State private var stickDir: Int = -1 @State private var padVector = CGSize.zero + private var solid: Bool { TouchControlStyle.isSolid(style) } private var diameter: CGFloat { TouchControlsModel.diameter(control) } private var size: CGSize { TouchControlsModel.size(control) } private var isStick: Bool { control.action.stickKeys != nil || control.action.isPadStick } @@ -4608,19 +4631,19 @@ struct TouchControlButton: View { } } .frame(width: size.width, height: size.height) - .glassFace(GlassShape(circle: true, tint: a.padTint)) + .glassFace(GlassShape(circle: true, tint: a.padTint, solid: solid)) case .small: Text(a.padFaceLabel) .font(.system(size: size.height * 0.36, weight: .semibold)) .foregroundStyle(.white.opacity(isDown ? 1.0 : 0.85)) .frame(width: size.width, height: size.height) - .glassFace(GlassShape(circle: true)) + .glassFace(GlassShape(circle: true, solid: solid)) case .wide: Text(a.padFaceLabel) .font(.system(size: size.height * 0.42, weight: .semibold)) .foregroundStyle(.white.opacity(isDown ? 1.0 : 0.88)) .frame(width: size.width, height: size.height) - .glassFace(GlassShape(cornerRadius: size.height * 0.30)) + .glassFace(GlassShape(cornerRadius: size.height * 0.30, solid: solid)) case .capsule: Text(a.padFaceLabel) .font(.system(size: size.height * 0.40, weight: .semibold)) @@ -4630,7 +4653,7 @@ struct TouchControlButton: View { .padding(.horizontal, 6) .foregroundStyle(.white.opacity(isDown ? 1.0 : 0.85)) .frame(width: size.width, height: size.height) - .glassFace(GlassShape(capsule: true)) + .glassFace(GlassShape(capsule: true, solid: solid)) } } @@ -4644,7 +4667,7 @@ struct TouchControlButton: View { Text(control.action.label).font(.caption).foregroundStyle(.white.opacity(0.8)) } .frame(width: diameter, height: diameter) - .glassFace(GlassShape(circle: true)) + .glassFace(GlassShape(circle: true, solid: solid)) } else if control.action.stickKeys != nil { // Reuse the portrait pad's face so both look and animate the // same; scale it to whatever size this control was pinched to. @@ -4656,7 +4679,7 @@ struct TouchControlButton: View { height: JoystickFace.padRadius * 2) .scaleEffect(diameter / (JoystickFace.padRadius * 2)) .frame(width: diameter, height: diameter) - .glassFace(GlassShape(circle: true)) + .glassFace(GlassShape(circle: true, solid: solid)) } else if control.action.isPad { padFace } else { @@ -4667,7 +4690,7 @@ struct TouchControlButton: View { weight: .medium)) .foregroundStyle(.white.opacity(isDown ? 1.0 : 0.85)) .frame(width: size.width, height: size.height) - .glassFace(GlassShape(circle: true)) + .glassFace(GlassShape(circle: true, solid: solid)) } } .frame(width: size.width, height: size.height) diff --git a/app/Madeira/Library.swift b/app/Madeira/Library.swift index f54dc4abf0..0ba8055c5b 100644 --- a/app/Madeira/Library.swift +++ b/app/Madeira/Library.swift @@ -5482,6 +5482,7 @@ struct LibraryHUD: View { } @ObservedObject private var model = LibraryModel.shared @ObservedObject private var controls = TouchControlsModel.shared + @AppStorage(TouchControlStyle.key) private var controlStyle = "glass" /// A Madeira Dock start: its status, failure and Show desktop (DockStartScreen). @ObservedObject private var dockStart = DockStartScreen.shared private let sessionTools = MadeiraConfig.flag("MADEIRA_SESSION_TOOLS") @@ -5857,6 +5858,12 @@ struct LibraryHUD: View { } LabeledContent("Opacity") { Slider(value: $model.opacity, in: 0.15...1) } LabeledContent("Size") { Slider(value: $controls.sizeScale, in: 0.5...2) } + // Glass or an opaque disc (TouchControlStyle); the same choice as Settings › Appearance. + LabeledContent("Button style") { + Picker("Button style", selection: $controlStyle) { + ForEach(TouchControlStyle.styles, id: \.id) { Text($0.name).tag($0.id) } + }.pickerStyle(.segmented).frame(maxWidth: 220) + } Button("Edit controls", systemImage: "slider.horizontal.3") { controls.visible = true; controls.editing = true; model.menu = false } if GamepadInput.keyboardMouseAvailable { ControllerModeChoice(mode: Binding(get: { model.controllerMode }, set: { model.controllerMode = $0; model.saveCurrentProfile() })) diff --git a/app/Madeira/SettingsPages.swift b/app/Madeira/SettingsPages.swift index 59002dc05b..646eb05595 100644 --- a/app/Madeira/SettingsPages.swift +++ b/app/Madeira/SettingsPages.swift @@ -137,6 +137,7 @@ struct SettingsPageView: View { @ObservedObject private var liquidMetal = LiquidMetalSetting.shared @AppStorage(LibraryGridArm.key) private var zoomableGrid = false @AppStorage(GridZoomHapticPlayer.key) private var zoomHaptics = true + @AppStorage(TouchControlStyle.key) private var controlStyle = "glass" @State private var developerUI = !FrontendChoice.preferNew @State private var restartNotice = false @@ -203,6 +204,13 @@ struct SettingsPageView: View { } footer: { Text("Flowing chrome on the bars and the Desktop button. Off, they use the system's Liquid Glass.") } + Section { + Picker("Touch control buttons", selection: $controlStyle) { + ForEach(TouchControlStyle.styles, id: \.id) { Text($0.name).tag($0.id) } + } + } footer: { + Text("Glass is the system's Liquid Glass. Solid draws each button as a plain opaque disc; a game's Opacity slider still fades them.") + } Section { Toggle("New library grid", isOn: $zoomableGrid) if zoomableGrid { diff --git a/docs/LIBRARY.md b/docs/LIBRARY.md index f22ffc5c24..aec0fd971c 100644 --- a/docs/LIBRARY.md +++ b/docs/LIBRARY.md @@ -314,7 +314,9 @@ in-game menu: 1. touch controls on/off, their **Controller layout** (the built-in Xbox controller, the user's custom layouts, **Create new layout**; remembered per - game), their **Opacity** and **Size**, **Edit controls** + game), their **Opacity** and **Size**, their **Button style** (Glass, the + system's Liquid Glass, or Solid, a plain opaque disc; remembered for every + game, also in Settings › Appearance), **Edit controls** (the existing editor) and the **Keyboard** (its own key window, with an Esc/Ctrl/Shift/Alt/Tab/Enter/arrow row; modifiers latch); 2. the FPS limit, **Aspect & scaling**, **Eco mode** (the developer overlay's diff --git a/tests/host/check-frontend.py b/tests/host/check-frontend.py index 900865c91f..7b263687b1 100644 --- a/tests/host/check-frontend.py +++ b/tests/host/check-frontend.py @@ -651,6 +651,19 @@ def block(text, header): check('if touchPointerMode { touchModeBegan(touches); return }' in content, 'the game view handles the Touch pointer mode') check('TouchControlsModel.diameter(control)' in content and 'library.opacity' in content, "touch controls follow the session's size and opacity") +# The Opacity slider: on iOS 26 a button's own .opacity inside the GlassEffectContainer +# left the glass as it was, so the opacity goes on the container (and the pre-26 group). +overlay = block(content, 'private func controls(_ screen: CGSize, session: Bool)') +check('let opacity = session && !m.editing ? library.opacity : 1' in overlay and '.opacity(opacity)' in overlay + and 'TouchControlButton(control: c, screen: screen)\n }' in overlay, + 'the Opacity slider fades the whole GlassEffectContainer, not each button inside it') +# Button style: Glass or Solid (TouchControlStyle), in the in-game menu and Settings › Appearance. +button = block(content, 'struct TouchControlButton: View') +check('@AppStorage(TouchControlStyle.key) private var style = "glass"' in button + and button.count('solid: solid') == 7 and 'if g.solid {' in content + and 'Picker("Button style", selection: $controlStyle)' in lib + and 'Picker("Touch control buttons", selection: $controlStyle)' in (root / 'app/Madeira/SettingsPages.swift').read_text(), + 'Button style: every control face takes Solid; chosen in the in-game menu and Settings › Appearance') # Owner requests: the starting screen's glyph row, Settings credits last, no drive_c note. launch = block(hud, 'private func launchColumnView(')