packages feed

nano-ui-sdl 0.1.0.0 → 0.1.0.1

raw patch · 11 files changed

+288/−204 lines, 11 filesdep ~Win32dep ~basedep ~bytestringPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

Dependency ranges changed: Win32, base, bytestring, containers, dir-traverse, directory, effectful-core, file-embed, filepath, hashable, primitive, record-hasfield, sdl3-bindgen-sys, tasty-bench, text, unordered-containers

API changes (from Hackage documentation)

+ NanoUI.Backend.Sdl: [sdlAppUiScale] :: SdlOptions -> Float
+ NanoUI.Backend.Sdl: [sdlUiScaleRef] :: SdlEnv -> IORef Float
+ NanoUI.Backend.Sdl: setSdlUiScale :: forall (es :: [Effect]). Ui :> es => Float -> Eff es ()
- NanoUI.Backend.Sdl: SdlEnv :: Ptr SDL_Window -> Ptr SDL_Renderer -> Text -> RenderBatch -> IORef NanoUIFont -> IORef NanoUIFont -> Maybe Float -> IORef Float -> GlyphAtlas -> ImageAtlas -> SdlCursors -> SdlDebugSampler -> IORef (Ptr SDL_Texture, Int, Int, Float) -> IORef Bool -> Bool -> Double -> Bool -> IORef Context -> SdlFontCache -> DialogState -> SdlEnv
+ NanoUI.Backend.Sdl: SdlEnv :: Ptr SDL_Window -> Ptr SDL_Renderer -> Text -> RenderBatch -> IORef NanoUIFont -> IORef NanoUIFont -> Maybe Float -> IORef Float -> IORef Float -> GlyphAtlas -> ImageAtlas -> SdlCursors -> SdlDebugSampler -> IORef Retain -> IORef Bool -> Bool -> Double -> Bool -> IORef Context -> SdlFontCache -> DialogState -> SdlEnv
- NanoUI.Backend.Sdl: SdlOptions :: Text -> Size -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> NanoUIFont -> NanoUIFont -> Float -> Maybe Theme -> (Input -> Bool) -> SmallArray RgbaImage -> SdlOptions
+ NanoUI.Backend.Sdl: SdlOptions :: Text -> Size -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> NanoUIFont -> NanoUIFont -> Float -> Maybe Theme -> (Input -> Bool) -> SmallArray RgbaImage -> Float -> SdlOptions
- NanoUI.Backend.Sdl: [sdlRetain] :: SdlEnv -> IORef (Ptr SDL_Texture, Int, Int, Float)
+ NanoUI.Backend.Sdl: [sdlRetain] :: SdlEnv -> IORef Retain

Files

CHANGELOG.md view
@@ -1,12 +1,61 @@ # Changelog +## Unreleased++### Added++- Shaped text. Lines are laid out by SDL_ttf and HarfBuzz with ligatures,+  contextual forms and marks, and drawn glyph by glyph from the atlas.+  Arabic, Hebrew, Devanagari, CJK and other scripts the UI font lacks are+  drawn from installed fallback fonts (Noto, DejaVu, and the Windows and+  macOS system fonts), found the first time a text needs them.++### Changed++- Bold and italic are drawn by the core's synthetic weight and slant over the+  regular face, since SDL_ttf's style flags do not match the glyph images+  shaped text draws. Text measurement uses the shaped width, so layout and+  drawing agree.+- Fallback fonts for other scripts are shared across font sizes and opened+  only when a text needs a character they cover. Each coverage font is+  opened once a session as a probe, and each size draws from a copy that+  shares the probe's file. Resolving 40 sizes that draw CJK, Arabic and+  Devanagari opens 59 file descriptors instead of 959.+- A shaped line is shaped once and shared by measuring, preparing and+  drawing, and the font directories are walked once per process.+- No longer depends on `vector`.+- The framebuffer, glyph rasterization and snapping follow the window's+  pixel density (`SDL_GetWindowPixelDensity`) instead of its display scale.+  On Windows window coordinates are already pixels, so at 125% scaling every+  frame drew 1.56 times the window's pixels and shrank them back when+  presenting. Frames now draw at the window's size and text is no longer+  resampled. Sizes are unchanged; geometry snaps to whole pixels rather+  than to the 1.25x grid, so edges can move by up to half a pixel.+- The retained framebuffer is allocated in 256 pixel blocks and reused while+  the window fits, so a resize drag no longer creates a new render target+  for every pixel the border moves.++### Fixed++- A resize drag no longer trails the border by a step. Each step was drawn+  on `SDL_EVENT_WINDOW_RESIZED`, before the renderer had resized its swap+  chain, so it went to the old-size backbuffer and showed cropped or with a+  bare strip; frames are now drawn on the pixel size change that follows.++### Removed++- `newSdlContext`; the runners create their own context.+- `isDebugActive`, `newSdlDebugSampler`, `readSdlDebug` and `takeDebugLive`+  from `NanoUI.Backend.Sdl`. The core event loop samples debug timing; read+  the readout with `askSdlDebug`.+- The font debugging exports `saveFontRenderText`, `queryFontKerning`,+  `queryFontPairKerning`, `debugFontPair` and `dumpFontLayout`.+- `NanoUI.Sdl.Session` is no longer an exposed module.++## 0.1.0.1 -- 2026-09-18++* + ## 0.1.0.0  First release.--- `runSdlApp` and `runSdlAppReduce` run nano-ui views in an SDL3 window.-- Text shaped with SDL_ttf and HarfBuzz, drawn from the bundled Inter font-  or installed fonts looked up by family name, with fallback fonts for-  scripts the UI font lacks.-- Clipboard, cursors, display scaling, images, and native file dialogs.-- The `simd` flag compiles the draw-batch culler with AVX2 on x86-64.
cbits/nano_ui_display.c view
@@ -48,8 +48,12 @@     if (!g_resize_cb || !event) {         return true;     }+    /* Not SDL_EVENT_WINDOW_RESIZED: SDL sends it just before the pixel size+     * change, which is what tells the renderer to resize its swap chain. A+     * frame drawn on RESIZED goes to the old-size backbuffer, shown cropped+     * or with a bare strip, and the size then counts as drawn, so the window+     * trails the drag by a step. */     if (event->type == SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED-        || event->type == SDL_EVENT_WINDOW_RESIZED         || event->type == SDL_EVENT_WINDOW_DISPLAY_SCALE_CHANGED) {         g_resize_cb();     }
− data/inter-LICENSE.txt
@@ -1,92 +0,0 @@-Copyright (c) 2016 The Inter Project Authors (https://github.com/rsms/inter)--This Font Software is licensed under the SIL Open Font License, Version 1.1.-This license is copied below, and is also available with a FAQ at:-http://scripts.sil.org/OFL--------------------------------------------------------------SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007--------------------------------------------------------------PREAMBLE-The goals of the Open Font License (OFL) are to stimulate worldwide-development of collaborative font projects, to support the font creation-efforts of academic and linguistic communities, and to provide a free and-open framework in which fonts may be shared and improved in partnership-with others.--The OFL allows the licensed fonts to be used, studied, modified and-redistributed freely as long as they are not sold by themselves. The-fonts, including any derivative works, can be bundled, embedded,-redistributed and/or sold with any software provided that any reserved-names are not used by derivative works. The fonts and derivatives,-however, cannot be released under any other type of license. The-requirement for fonts to remain under this license does not apply-to any document created using the fonts or their derivatives.--DEFINITIONS-"Font Software" refers to the set of files released by the Copyright-Holder(s) under this license and clearly marked as such. This may-include source files, build scripts and documentation.--"Reserved Font Name" refers to any names specified as such after the-copyright statement(s).--"Original Version" refers to the collection of Font Software components as-distributed by the Copyright Holder(s).--"Modified Version" refers to any derivative made by adding to, deleting,-or substituting -- in part or in whole -- any of the components of the-Original Version, by changing formats or by porting the Font Software to a-new environment.--"Author" refers to any designer, engineer, programmer, technical-writer or other person who contributed to the Font Software.--PERMISSION AND CONDITIONS-Permission is hereby granted, free of charge, to any person obtaining-a copy of the Font Software, to use, study, copy, merge, embed, modify,-redistribute, and sell modified and unmodified copies of the Font-Software, subject to the following conditions:--1) Neither the Font Software nor any of its individual components,-in Original or Modified Versions, may be sold by itself.--2) Original or Modified Versions of the Font Software may be bundled,-redistributed and/or sold with any software, provided that each copy-contains the above copyright notice and this license. These can be-included either as stand-alone text files, human-readable headers or-in the appropriate machine-readable metadata fields within text or-binary files as long as those fields can be easily viewed by the user.--3) No Modified Version of the Font Software may use the Reserved Font-Name(s) unless explicit written permission is granted by the corresponding-Copyright Holder. This restriction only applies to the primary font name as-presented to the users.--4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font-Software shall not be used to promote, endorse or advertise any-Modified Version, except to acknowledge the contribution(s) of the-Copyright Holder(s) and the Author(s) or with their explicit written-permission.--5) The Font Software, modified or unmodified, in part or in whole,-must be distributed entirely under this license, and must not be-distributed under any other license. The requirement for fonts to-remain under this license does not apply to any document created-using the Font Software.--TERMINATION-This license becomes null and void if any of the above conditions are-not met.--DISCLAIMER-THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,-EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF-MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT-OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE-COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,-INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL-DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING-FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM-OTHER DEALINGS IN THE FONT SOFTWARE.
lib/NanoUI/Backend/Sdl.hs view
@@ -6,6 +6,7 @@   , SdlOptions (..)   , askSdlDebug   , setSdlUiFont+  , setSdlUiScale   , defaultSdlOptions   , FileFilter (..)   , FileDialogOptions (..)@@ -38,7 +39,7 @@ import Data.Primitive.SmallArray (SmallArray) import Data.Typeable (Typeable) import NanoUI (NanoUI)-import NanoUI.Sdl.Runner (askSdlDebug, drawReduceEff, sdlDrawFrame, setSdlUiFont)+import NanoUI.Sdl.Runner (askSdlDebug, drawReduceEff, sdlDrawFrame, setSdlUiFont, setSdlUiScale) import NanoUI.Sdl.Session (runSdlSession) import NanoUI.Sdl.Debug (SdlDebugSnapshot (..)) import NanoUI.Sdl.Window (RgbaImage (..), SdlEnv (..), SdlOptions (..), defaultSdlOptions, saveScreenshot, syncDisplay, withSdl, withSdlBench)
lib/NanoUI/Sdl/Display.hs view
@@ -2,10 +2,11 @@  module NanoUI.Sdl.Display   ( defaultFontSize-  , queryWindowDisplayScale+  , queryWindowPixelDensity   , queryWindowRefreshHz   , queryWindowLogicalSize   , queryMouseWindowPos+  , zoomWindow   , installResizeWatch   , refreshEventType   , initRefreshEvent@@ -25,15 +26,23 @@ import SDL3.Sys.Bindgen.Video (SDL_Window) import SDL3.Sys.Events (pushEvent, registerEvents) import SDL3.Sys.Mouse (getMouseState)-import SDL3.Sys.Video (getWindowDisplayScale, getWindowSize)+import SDL3.Sys.Bindgen.Rect (SDL_Rect (..))+import SDL3.Sys.Video (getDisplayForWindow, getDisplayUsableBounds, getWindowPixelDensity, getWindowSize, setWindowPosition, setWindowSize) import System.IO.Unsafe (unsafePerformIO)  defaultFontSize :: Float defaultFontSize = 16 -queryWindowDisplayScale :: Ptr SDL_Window -> IO Float-queryWindowDisplayScale win = do-  s <- getWindowDisplayScale win+-- | Backbuffer pixels per window coordinate: the factor the retained+-- framebuffer, glyph rasterization and snapping need. This is not+-- 'SDL_GetWindowDisplayScale', which also folds in the desktop's content+-- scale. On Windows window coordinates are already pixels, so at 125%+-- scaling the display scale is 1.25 while the density is 1; sizing the+-- framebuffer by the display scale rendered 1.56x the window's pixels and+-- squeezed them back down on every present.+queryWindowPixelDensity :: Ptr SDL_Window -> IO Float+queryWindowPixelDensity win = do+  s <- getWindowPixelDensity win   pure (if s > 0 then s else 1)  -- | Vertical refresh rate of the window's current display mode, in Hz@@ -122,3 +131,20 @@  foreign import ccall safe "nano_ui_remove_resize_watch"   removeResizeWatchC :: IO ()++-- | Grow a window just opened at a logical size by the UI zoom, as far as+-- the usable area of its display allows, and centre it again.+zoomWindow :: Ptr SDL_Window -> Size -> Float -> IO ()+zoomWindow win (Size w h) zoom = do+  display <- getDisplayForWindow win+  usable <- alloca $ \rp -> do+    ok <- getDisplayUsableBounds display rp+    if ok then Just <$> peek rp else pure Nothing+  let fit want avail = case avail of+        Just a | a > 0 -> min want (fromIntegral a)+        _ -> want+      zw = fit (w * zoom) ((.w) <$> usable)+      zh = fit (h * zoom) ((.h) <$> usable)+      centred = 0x2FFF0000 -- SDL_WINDOWPOS_CENTERED+  void $ setWindowSize win (round zw) (round zh)+  void $ setWindowPosition win centred centred
lib/NanoUI/Sdl/Font.hs view
@@ -1272,12 +1272,12 @@   , sfcGlyphAtlas     :: !GlyphAtlas   , sfcBasePt         :: !Float   , sfcScaleRef       :: !(IORef Float)-  -- ^ The display scale, owned by the window and read here.+  -- ^ The window pixel density, owned by the window and read here.   , sfcBaseEntries    :: !(IORef (CachedFontEntry, CachedFontEntry))   , sfcDynamicCache   :: !(IORef (BoundedCache FontCacheKey CachedFontEntry))   } --- | Open the base sans and mono fonts at the display scale, and re-warm them+-- | Open the base sans and mono fonts at the pixel density, and re-warm them -- into the glyph atlas after every atlas reset. newSdlFontCache ::   FontSource -> -- ^ primary font source@@ -1286,7 +1286,7 @@   FontSource -> -- ^ mono fallback font source   GlyphAtlas ->   Float ->      -- ^ base font size (pt)-  IORef Float -> -- ^ display scale+  IORef Float -> -- ^ pixel density   IO SdlFontCache newSdlFontCache primary fallback mono monoFb ga basePt scaleRef = do   scale <- readIORef scaleRef@@ -1317,7 +1317,7 @@       }  -- | A font from a source (or its fallback) at a point size, rasterised at--- the display scale, with its glyph metrics.+-- the pixel density, with its glyph metrics. openCachedFont :: GlyphAtlas -> Float -> FontSource -> FontSource -> Float -> IO CachedFontEntry openCachedFont ga scale primary fallback pt = do   font <- openFontSourceWithFallback primary fallback (pt * scale)@@ -1344,7 +1344,7 @@   (sans, mono) <- readIORef (sfcBaseEntries cache)   closeCachedFonts (sfcGlyphAtlas cache) (cfeFont sans : cfeFont mono : map cfeFont (HM.elems (bcEntries dynamic))) --- | Reopen the base fonts from @source@ at the current display scale, close+-- | Reopen the base fonts from @source@ at the current pixel density, close -- every dynamic size, and reset the glyph atlas, which re-warms the new base -- fonts. reloadSdlFontCache :: SdlFontCache -> FontSource -> IO ()
lib/NanoUI/Sdl/Image.hs view
@@ -15,9 +15,8 @@ import Foreign.Ptr (Ptr, castPtr, nullPtr) import NanoUI.Testing (Context, atlasSnapshot, atlasTextureId) import SDL3.Sys.Bindgen.Blendmode (sDL_BLENDMODE_BLEND)-import SDL3.Sys.Bindgen.Pixels qualified as Pixels-import SDL3.Sys.Bindgen.Render (SDL_Renderer, SDL_Texture)-import SDL3.Sys.Bindgen.Render qualified as Render+import SDL3.Sys.Bindgen.Pixels (data SDL_PIXELFORMAT_RGBA32)+import SDL3.Sys.Bindgen.Render (SDL_Renderer, SDL_Texture, data SDL_TEXTUREACCESS_STATIC) import SDL3.Sys.Bindgen.Runtime.PtrConst qualified as PtrConst import SDL3.Sys.Render (createTextureSafe, destroyTexture, setTextureBlendMode, updateTextureSafe) @@ -52,7 +51,7 @@   Ptr SDL_Renderer -> ImageAtlas -> Int -> Int -> ForeignPtr Word8 -> Int -> IO () uploadAtlas ren (ImageAtlas ref) w h pixels gen = mask_ $   withForeignPtr pixels $ \ptr -> do-    tex <- createTextureSafe ren Pixels.SDL_PIXELFORMAT_RGBA32 Render.SDL_TEXTUREACCESS_STATIC (fromIntegral w) (fromIntegral h)+    tex <- createTextureSafe ren SDL_PIXELFORMAT_RGBA32 SDL_TEXTUREACCESS_STATIC (fromIntegral w) (fromIntegral h)     ok <-       if tex == nullPtr         then pure False
lib/NanoUI/Sdl/Input.hs view
@@ -42,8 +42,24 @@   ( SDL_Event (..)   , SDL_EventType (..)   , SDL_KeyboardEvent+  , data SDL_EVENT_DROP_BEGIN+  , data SDL_EVENT_DROP_COMPLETE+  , data SDL_EVENT_DROP_FILE+  , data SDL_EVENT_DROP_POSITION+  , data SDL_EVENT_DROP_TEXT+  , data SDL_EVENT_KEY_DOWN+  , data SDL_EVENT_MOUSE_BUTTON_DOWN+  , data SDL_EVENT_MOUSE_BUTTON_UP+  , data SDL_EVENT_MOUSE_MOTION+  , data SDL_EVENT_MOUSE_WHEEL+  , data SDL_EVENT_QUIT+  , data SDL_EVENT_TEXT_INPUT+  , data SDL_EVENT_WINDOW_DISPLAY_SCALE_CHANGED+  , data SDL_EVENT_WINDOW_EXPOSED+  , data SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED+  , data SDL_EVENT_WINDOW_RESIZED+  , data SDL_EVENT_WINDOW_RESTORED   )-import SDL3.Sys.Bindgen.Events qualified as Events import SDL3.Sys.Events (pollEventSafe, waitEventSafe, waitEventTimeoutSafe) import SDL3.Sys.Bindgen.Keycode   ( SDL_Keycode (..)@@ -116,27 +132,27 @@   if refreshTy /= 0 && w == refreshTy     then pure (Just EvRefresh)     else case SDL_EventType (fromIntegral w) of-      Events.SDL_EVENT_QUIT -> pure (Just EvQuit)-      Events.SDL_EVENT_WINDOW_RESIZED -> Just <$> windowResized p+      SDL_EVENT_QUIT -> pure (Just EvQuit)+      SDL_EVENT_WINDOW_RESIZED -> Just <$> windowResized p       -- Pixel size changes are ignored here; syncDisplay re-queries logical size.-      Events.SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED -> pure (Just EvDisplayScale)-      Events.SDL_EVENT_WINDOW_DISPLAY_SCALE_CHANGED -> pure (Just EvDisplayScale)+      SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED -> pure (Just EvDisplayScale)+      SDL_EVENT_WINDOW_DISPLAY_SCALE_CHANGED -> pure (Just EvDisplayScale)       -- The window manager damaged our window surface (occlusion, compositor       -- effects, restore). The backbuffer contents are gone; the next present       -- must be full or stale regions flash.-      Events.SDL_EVENT_WINDOW_EXPOSED -> pure (Just EvWindowRedraw)-      Events.SDL_EVENT_WINDOW_RESTORED -> pure (Just EvWindowRedraw)-      Events.SDL_EVENT_KEY_DOWN -> keyDown p-      Events.SDL_EVENT_TEXT_INPUT -> textInput p-      Events.SDL_EVENT_MOUSE_MOTION -> Just <$> mouseMotion p-      Events.SDL_EVENT_MOUSE_BUTTON_DOWN -> mouseButton p True-      Events.SDL_EVENT_MOUSE_BUTTON_UP -> mouseButton p False-      Events.SDL_EVENT_MOUSE_WHEEL -> Just <$> mouseWheel p-      Events.SDL_EVENT_DROP_FILE -> Just <$> dropEvent p DropFile-      Events.SDL_EVENT_DROP_TEXT -> Just <$> dropEvent p DropText-      Events.SDL_EVENT_DROP_BEGIN -> Just <$> dropEvent p DropBegin-      Events.SDL_EVENT_DROP_COMPLETE -> Just <$> dropEvent p DropComplete-      Events.SDL_EVENT_DROP_POSITION -> Just <$> dropEvent p DropPosition+      SDL_EVENT_WINDOW_EXPOSED -> pure (Just EvWindowRedraw)+      SDL_EVENT_WINDOW_RESTORED -> pure (Just EvWindowRedraw)+      SDL_EVENT_KEY_DOWN -> keyDown p+      SDL_EVENT_TEXT_INPUT -> textInput p+      SDL_EVENT_MOUSE_MOTION -> Just <$> mouseMotion p+      SDL_EVENT_MOUSE_BUTTON_DOWN -> mouseButton p True+      SDL_EVENT_MOUSE_BUTTON_UP -> mouseButton p False+      SDL_EVENT_MOUSE_WHEEL -> Just <$> mouseWheel p+      SDL_EVENT_DROP_FILE -> Just <$> dropEvent p DropFile+      SDL_EVENT_DROP_TEXT -> Just <$> dropEvent p DropText+      SDL_EVENT_DROP_BEGIN -> Just <$> dropEvent p DropBegin+      SDL_EVENT_DROP_COMPLETE -> Just <$> dropEvent p DropComplete+      SDL_EVENT_DROP_POSITION -> Just <$> dropEvent p DropPosition       _ -> pure Nothing  keyDown :: Ptr SDL_Event -> IO (Maybe SdlEvent)
lib/NanoUI/Sdl/Runner.hs view
@@ -6,6 +6,7 @@   , drawReduceEff   , askSdlDebug   , setSdlUiFont+  , setSdlUiScale   ) where  import Control.Exception (finally, mask_)@@ -46,7 +47,7 @@   , emptySdlDebug   , traceFrame   )-import NanoUI.Sdl.Display (queryMouseWindowPos, queryWindowLogicalSize)+import NanoUI.Sdl.Display (pushRefreshEvent, queryMouseWindowPos, queryWindowLogicalSize) import NanoUI.Sdl.Font   ( fontSourceLabel   , glyphAtlasTexture@@ -56,15 +57,16 @@   ) import NanoUI.Sdl.NanoUIFont (NanoUIFont) import NanoUI.Sdl.Render (flushRenderBatch, renderDrawDataPass, snapDamage)-import NanoUI.Sdl.Window (SdlEnv (..))+import NanoUI.Sdl.Window (Retain (..), SdlEnv (..)) import Foreign.Marshal.Alloc (alloca)+import Foreign.Marshal.Utils (with) import Foreign.Ptr (Ptr, nullPtr) import Foreign.Storable (peek) import qualified NanoUI.Sdl.Image as SdlImage import SDL3.Sys.Bindgen.Blendmode (sDL_BLENDMODE_NONE)-import SDL3.Sys.Bindgen.Pixels qualified as Pixels-import SDL3.Sys.Bindgen.Render (SDL_Texture)-import SDL3.Sys.Bindgen.Render qualified as Render+import SDL3.Sys.Bindgen.Pixels (data SDL_PIXELFORMAT_RGBA32)+import SDL3.Sys.Bindgen.Rect (SDL_FRect (..))+import SDL3.Sys.Bindgen.Render (SDL_Texture, data SDL_TEXTUREACCESS_TARGET) import SDL3.Sys.Bindgen.Runtime.PtrConst qualified as PtrConst import SDL3.Sys.Render   ( createTexture@@ -214,7 +216,11 @@             okTarget <- setRenderTarget (sdlRenderer env) nullPtr             okClip <- setRenderClipRect (sdlRenderer env) (PtrConst.unsafeFromPtr nullPtr)             void $ setRenderScale (sdlRenderer env) 1 1-            okCopy <- renderTexture (sdlRenderer env) tex (PtrConst.unsafeFromPtr nullPtr) (PtrConst.unsafeFromPtr nullPtr)+            -- The texture is larger than the window: copy only the used area.+            r <- readIORef (sdlRetain env)+            let src = SDL_FRect 0 0 (fromIntegral (retainW r)) (fromIntegral (retainH r))+            okCopy <- with src $ \srcP ->+              renderTexture (sdlRenderer env) tex (PtrConst.unsafeFromPtr srcP) (PtrConst.unsafeFromPtr nullPtr)             pure (okTarget && okClip && okCopy)       unless okBlit $ fail "SDL window presentation preparation failed"       void $ renderPresentSafe (sdlRenderer env)@@ -227,23 +233,36 @@       writeIORef (sdlLastPresented env) True       pure (dirtyAfterUi, inp) +-- | Pixels a retained texture is rounded up to, in each dimension.+retainBlock :: Int+retainBlock = 256+ ensureRetain :: SdlEnv -> Int -> Int -> Float -> IO (Ptr SDL_Texture, Bool) ensureRetain env w h scale = do-  (tex, ow, oh, oldScale) <- readIORef (sdlRetain env)-  let scaleChanged = abs (oldScale - scale) > 0.001-  if tex /= nullPtr && ow == w && oh == h+  r <- readIORef (sdlRetain env)+  let tex = retainTexture r+      fits = w <= retainCapW r && h <= retainCapH r+      -- Give memory back once the window is well inside the texture, but not+      -- for a shrink of a block or so, which a drag back out would undo.+      roomy = retainCapW r - w > 2 * retainBlock || retainCapH r - h > 2 * retainBlock+      -- A new used size or density holds none of the frame to be drawn.+      stale = w /= retainW r || h /= retainH r || abs (retainScale r - scale) > 0.001+  if tex /= nullPtr && fits && not roomy     then do-      when scaleChanged $ writeIORef (sdlRetain env) (tex, w, h, scale)-      -- Same pixel size after a DPI change still holds the old present.-      pure (tex, scaleChanged)+      when stale $ writeIORef (sdlRetain env) r {retainW = w, retainH = h, retainScale = scale}+      pure (tex, stale)     else mask_ $ do+      let cw = roundUp w+          ch = roundUp h       -- Allocate before replacing: failure leaves the owned texture valid.-      tex' <- createTexture (sdlRenderer env) Pixels.SDL_PIXELFORMAT_RGBA32 Render.SDL_TEXTUREACCESS_TARGET (fromIntegral w) (fromIntegral h)+      tex' <- createTexture (sdlRenderer env) SDL_PIXELFORMAT_RGBA32 SDL_TEXTUREACCESS_TARGET (fromIntegral cw) (fromIntegral ch)       when (tex' == nullPtr) $ fail "SDL_CreateTexture(retain) failed"       void $ setTextureBlendMode tex' (fromIntegral sDL_BLENDMODE_NONE)-      writeIORef (sdlRetain env) (tex', w, h, scale)+      writeIORef (sdlRetain env) (Retain tex' cw ch w h scale)       unless (tex == nullPtr) $ destroyTexture tex       pure (tex', True)+  where+    roundUp n = max retainBlock (((n + retainBlock - 1) `div` retainBlock) * retainBlock)  askSdlDebug :: Ui :> es => Eff es SdlDebugSnapshot askSdlDebug = do@@ -281,3 +300,18 @@     Just env -> uiIO $ do       cur <- readIORef (sdlFontRequestRef env)       when (cur /= font) $ writeIORef (sdlFontRequestRef env) font++-- | Set the UI scale (see 'NanoUI.Sdl.Window.sdlAppUiScale'): a zoom on top+-- of the pixel density, or zero or less to follow the display. The display+-- thread applies it before the next frame, which this wakes. A no-op on+-- non-SDL hosts.+setSdlUiScale :: Ui :> es => Float -> Eff es ()+setSdlUiScale s = do+  menv <- askHost @SdlEnv+  case menv of+    Nothing -> pure ()+    Just env -> uiIO $ do+      cur <- readIORef (sdlUiScaleRef env)+      when (cur /= s) $ do+        writeIORef (sdlUiScaleRef env) s+        pushRefreshEvent
lib/NanoUI/Sdl/Window.hs view
@@ -1,6 +1,8 @@ module NanoUI.Sdl.Window   ( RgbaImage (..)   , SdlEnv (..)+  , Retain (..)+  , noRetain   , SdlOptions (..)   , defaultSdlOptions   , withSdl@@ -25,7 +27,7 @@ import Foreign.Marshal.Alloc (alloca) import Foreign.Ptr (Ptr, nullPtr) import Foreign.Storable (peek)-import NanoUI (ImageId, Input (..), Size (..), Theme)+import NanoUI (ImageId, Input (..), Size (..), Theme, V2 (..)) import NanoUI.Context (Context (..), setDrawSnapScale) import NanoUI.Testing (clearMeasureCache, markDirty, setHost, setWakeLoop) import NanoUI.Sdl.Display@@ -33,9 +35,10 @@   , initRefreshEvent   , pushRefreshEvent   , queryMouseWindowPos-  , queryWindowDisplayScale+  , queryWindowPixelDensity   , queryWindowLogicalSize   , queryWindowRefreshHz+  , zoomWindow   ) import NanoUI.Sdl.Clipboard (withSdlClipboard) import NanoUI.Sdl.Cursor (SdlCursors (..), destroyCursors, initCursors)@@ -81,7 +84,7 @@   , setRenderVSync   ) import SDL3.Sys.Surface (destroySurface, saveBMP)-import SDL3.Sys.Video (destroyWindowSafe)+import SDL3.Sys.Video (destroyWindowSafe, getWindowDisplayScale)  -- | Initial RGBA asset uploaded before the first frame. data RgbaImage = RgbaImage@@ -123,6 +126,11 @@   -- ^ Predicate on user input to trigger application exit (default: @const False@).\   , sdlAppImages :: !(SmallArray RgbaImage)   -- ^ Initial RGBA textures registered before the first frame.+  , sdlAppUiScale :: !Float+  -- ^ Zoom of the whole UI, on top of the window's pixel density (default:+  -- 1). Zero or less follows the display's content scale, which on Windows+  -- is the desktop's scaling setting. 'NanoUI.Backend.Sdl.setSdlUiScale'+  -- changes it at runtime.   }  defaultSdlOptions :: SdlOptions@@ -143,14 +151,17 @@     , sdlAppTheme = Nothing     , sdlAppShouldQuit = const False     , sdlAppImages = mempty+    , sdlAppUiScale = 1     }  -- | SDL_WINDOW_HIGH_PIXEL_DENSITY (0x2000): without it the window's surface -- gets scale 1.0 even on a 2x / HiDPI output, so the compositor upscales the--- whole window (blurry "looks upscaled"). With it, SDL_GetWindowDisplayScale--- returns the real output scale, the window keeps its logical size, and the--- pixel buffer (and therefore the retain texture, glyph atlas, and fonts)--- rasterizes at the native pixel density.+-- whole window (blurry "looks upscaled"). With it, SDL_GetWindowPixelDensity+-- returns the real output scale where window coordinates are points (macOS,+-- Wayland), the window keeps its logical size, and the pixel buffer (and+-- therefore the retain texture, glyph atlas, and fonts) rasterizes at the+-- native pixel density. On Windows window coordinates are already pixels, so+-- the density stays 1 whatever the desktop scaling. windowFlags :: SdlOptions -> SDL_WindowFlags windowFlags opts =   SDL_WindowFlags $@@ -178,13 +189,16 @@   , sdlFontRequestRef :: !(IORef NanoUIFont)   , sdlFontAppliedRef :: !(IORef NanoUIFont)   , sdlForcedScale :: !(Maybe Float)-  -- ^ NANO_FORCE_SCALE override of the display scale, read at startup.+  -- ^ NANO_FORCE_SCALE override of the pixel density, read at startup.   , sdlScaleRef :: IORef Float+  -- ^ Backbuffer pixels per layout unit: the pixel density times the zoom.+  , sdlUiScaleRef :: !(IORef Float)+  -- ^ The requested UI scale; see 'sdlAppUiScale'.   , sdlGlyphAtlas :: GlyphAtlas   , sdlImages :: ImageAtlas   , sdlCursors :: SdlCursors   , sdlDebug :: SdlDebugSampler-  , sdlRetain :: IORef (Ptr SDL_Texture, Int, Int, Float)+  , sdlRetain :: IORef Retain   , sdlLastPresented :: IORef Bool   , sdlVsync :: !Bool   , sdlRefreshPeriod :: !Double@@ -194,18 +208,48 @@   , sdlDialogState :: !DialogState   } +-- | The retained framebuffer. The texture is allocated in blocks larger than+-- the window, so a resize drag reuses it instead of creating a render target+-- per pixel of movement; only the top-left used area is drawn and presented.+data Retain = Retain+  { retainTexture :: !(Ptr SDL_Texture)+  , retainCapW :: !Int+  , retainCapH :: !Int+  -- ^ The texture's allocated size in pixels.+  , retainW :: !Int+  , retainH :: !Int+  -- ^ The pixel size the last frame used.+  , retainScale :: !Float+  }++noRetain :: Retain+noRetain = Retain nullPtr 0 0 0 0 0+ defaultWindowSize :: Size defaultWindowSize = Size 1280 800 --- Layout in logical coordinates; draw/text rasterize at native pixel density.+-- | The zoom a UI scale setting asks for: the setting itself, or for zero or+-- less the display's content scale beyond the pixel density.+resolveZoom :: Ptr SDL_Window -> Float -> IO Float+resolveZoom win setting+  | setting > 0 = pure setting+  | otherwise = do+      display <- getWindowDisplayScale win+      density <- queryWindowPixelDensity win+      pure (if display > 0 then max 0.25 (display / density) else 1)++-- Layout in logical coordinates (window coordinates over the zoom);+-- draw/text rasterize at native pixel density. syncDisplay :: Context -> SdlEnv -> Input -> IO (Context, Input) syncDisplay ctx env inp = do-  scale <- maybe (queryWindowDisplayScale (sdlWindow env)) pure (sdlForcedScale env)+  density <- maybe (queryWindowPixelDensity (sdlWindow env)) pure (sdlForcedScale env)+  zoom <- resolveZoom (sdlWindow env) =<< readIORef (sdlUiScaleRef env)+  let scale = density * zoom   oldScale <- readIORef (sdlScaleRef env)   let scaleChanged = abs (scale - oldScale) > scaleEpsilon   when scaleChanged $ do     -- Presents leave the renderer at 1:1 pixels; re-assert it only when the-    -- display scale moves.+    -- pixel density moves.     ok <- setRenderScale (sdlRenderer env) 1 1     unless ok $ fail "SDL_SetRenderScale failed"     writeIORef (sdlScaleRef env) scale@@ -227,14 +271,16 @@     clearMeasureCache ctx     markDirty ctx   queried <- queryWindowLogicalSize (sdlWindow env)-  let winSize =-        case queried of+  let unzoom (Size sw sh) = Size (sw / zoom) (sh / zoom)+      winSize =+        case unzoom queried of           Size 0 0 ->             case inputWindowSize inp of               Size 0 0 -> defaultWindowSize               s -> s           s -> s-  mouse <- queryMouseWindowPos+  V2 mx my <- queryMouseWindowPos+  let mouse = V2 (mx / zoom) (my / zoom)   ctxMeasured <- readIORef (sdlCachedCtx env)   pure (ctxMeasured, inp {inputWindowSize = winSize, inputMousePos = mouse}) @@ -250,6 +296,7 @@   , wcUiFont :: !NanoUIFont   , wcMonoFont :: !NanoUIFont   , wcFontSize :: !Float+  , wcUiScale :: !Float   }  withSdl :: SdlOptions -> Context -> (Context -> SdlEnv -> IO a) -> IO a@@ -266,6 +313,7 @@       , wcUiFont = sdlAppFont opts       , wcMonoFont = sdlAppMonoFont opts       , wcFontSize = sdlAppFontSize opts+      , wcUiScale = sdlAppUiScale opts       }  withSdlBench :: Context -> (Context -> SdlEnv -> IO a) -> IO a@@ -282,6 +330,7 @@       , wcUiFont = DefaultFont       , wcMonoFont = DefaultFont       , wcFontSize = defaultFontSize+      , wcUiScale = 1       }  withSdlWindow :: Context -> WindowConfig -> (Context -> SdlEnv -> IO a) -> IO a@@ -324,7 +373,7 @@   unless refreshOk $ fail "SDL_RegisterEvents failed for refresh wake"   let Size w h = wcSize cfg       bench = wcBench cfg-  -- NANO_FORCE_SCALE: debug override of the display scale.+  -- NANO_FORCE_SCALE: debug override of the pixel density.   forcedEnv <- lookupEnv "NANO_FORCE_SCALE"   let forcedScale = case forcedEnv >>= readMaybe of         Just s | s > 0 -> Just s@@ -344,19 +393,25 @@           unless ok $ fail "SDL_CreateWindowAndRenderer failed"           win <- peek winPtr           ren <- peek renPtr-          scale <- queryWindowDisplayScale win+          density <- queryWindowPixelDensity win+          zoom <- resolveZoom win (wcUiScale cfg)+          -- The requested size is logical, so the window grows with the zoom.+          when (abs (zoom - 1) > scaleEpsilon) $+            zoomWindow win (wcSize cfg) zoom+          let scale = density * zoom           setDrawSnapScale ctx scale           refreshHz <- queryWindowRefreshHz win           rendererName <- getRendererName ren >>= \name ->             if PtrConst.unsafeToPtr name == nullPtr then pure "unknown" else TextForeign.peekCString (PtrConst.unsafeToPtr name)           scaleRef <- newIORef scale+          uiScaleRef <- newIORef (wcUiScale cfg)           fontRequestRef <- newIORef (wcUiFont cfg)           fontAppliedRef <- newIORef (wcUiFont cfg)           glyphAtlas <- newGlyphAtlas ren           images <- newImageAtlas           cursors <- initCursors           debug <- newSdlDebugSampler-          retain <- newIORef (nullPtr, 0, 0, 0)+          retain <- newIORef noRetain           fontCache <-             newSdlFontCache               fontSource@@ -389,6 +444,7 @@               , sdlFontAppliedRef = fontAppliedRef               , sdlForcedScale = forcedScale               , sdlScaleRef = scaleRef+              , sdlUiScaleRef = uiScaleRef               , sdlGlyphAtlas = glyphAtlas               , sdlImages = images               , sdlCursors = cursors@@ -410,7 +466,7 @@ stopSdlWindow :: Bool -> SdlEnv -> IO () stopSdlWindow bench env = do   clearDialogState (sdlDialogState env)-  (tex, _, _, _) <- readIORef (sdlRetain env)+  tex <- retainTexture <$> readIORef (sdlRetain env)   unless (tex == nullPtr) $ destroyTexture tex   destroyRenderBatch (sdlBatch env)   destroyCursors (sdlCursors env)
nano-ui-sdl.cabal view
@@ -1,33 +1,24 @@ cabal-version:      3.4 name:               nano-ui-sdl-version:            0.1.0.0+version:            0.1.0.1 synopsis:           SDL3 window backend for nano-ui description:     Runs nano-ui views in an SDL3 window with TrueType text and native file     dialogs. Requires SDL3, SDL3_ttf, and pkg-config.-license:            MIT AND OFL-1.1+license:            MIT license-file:       LICENSE author:             goolord maintainer:         zacharyachurchill@gmail.com category:           Graphics-homepage:           https://github.com/goolord/nano-ui-bug-reports:        https://github.com/goolord/nano-ui/issues build-type:         Simple-tested-with:        GHC ==9.10.3 || ==9.14.1 extra-doc-files:     CHANGELOG.md     README.md-    data/inter-LICENSE.txt  extra-source-files:     data/inter.ttf     cbits/*.h -source-repository head-    type:     git-    location: https://github.com/goolord/nano-ui.git-    subdir:   packages/nano-ui-sdl- flag sdl     description: Build the SDL3 backend. Needs SDL3, SDL3_ttf, and pkg-config.     manual: True@@ -92,21 +83,21 @@         NanoUI.Sdl.Render         NanoUI.Sdl.Window     build-depends:-        base >=4.20 && <4.23,-        bytestring >=0.11 && <0.13,-        containers >=0.6.7 && <0.9,-        directory >=1.3.7 && <1.4,-        dir-traverse >=0.2.3 && <0.3,-        effectful-core >=2.5 && <2.8,-        filepath >=1.4.100 && <1.6,-        file-embed >=0.0.16 && <0.1,-        hashable >=1.4 && <1.6,+        base ^>=4.22.0.0,+        bytestring ^>=0.12,+        containers ^>=0.8,+        directory ^>=1.3,+        dir-traverse ^>=0.2.3.0,+        effectful-core ^>=2.6,+        filepath ^>=1.5,+        file-embed ^>=0.0.16,+        hashable ^>=1.5,         nano-ui ^>=0.1,-        primitive >=0.8 && <0.10,-        record-hasfield >=1.0 && <1.1,-        sdl3-bindgen-sys >=0.0.0.3 && <0.0.1,-        text >=2.1.2 && <2.2,-        unordered-containers >=0.2.19 && <0.3+        primitive ^>=0.9,+        record-hasfield ^>=1.0,+        sdl3-bindgen-sys ^>=0.0.0.2,+        text ^>=2.1,+        unordered-containers ^>=0.2     hs-source-dirs:   lib     if !flag(sdl)         buildable: False@@ -128,11 +119,11 @@     hs-source-dirs:   test, lib     other-modules:    NanoUI.Sdl.Font.Search     build-depends:-        base >=4.20 && <4.23,-        containers >=0.6.7 && <0.9,-        directory >=1.3.7 && <1.4,-        dir-traverse >=0.2.3 && <0.3,-        filepath >=1.4.100 && <1.6+        base ^>=4.22.0.0,+        containers ^>=0.8,+        directory ^>=1.3,+        dir-traverse ^>=0.2.3.0,+        filepath ^>=1.5  test-suite nano-ui-font-effects-test     import:           extensions, warnings, rts-options@@ -140,11 +131,11 @@     main-is:          FontEffects.hs     hs-source-dirs:   test     build-depends:-        base >=4.20 && <4.23,+        base ^>=4.22.0.0,         nano-ui,         nano-ui-sdl,-        primitive >=0.8 && <0.10,-        text >=2.1.2 && <2.2+        primitive ^>=0.9,+        text ^>=2.1     if !flag(sdl)         buildable: False @@ -154,10 +145,10 @@     import:           warnings     main-is:          SdlAnim.hs     build-depends:-        base >=4.20 && <4.23,+        base ^>=4.22.0.0,         nano-ui,         nano-ui-sdl,-        text >=2.1.2 && <2.2+        text ^>=2.1     hs-source-dirs:   examples     if os(windows)         ghc-options: -optl-mconsole@@ -172,12 +163,12 @@     type:             exitcode-stdio-1.0     main-is:          SdlBench.hs     build-depends:-        base >=4.20 && <4.23,+        base ^>=4.22.0.0,         nano-ui,         nano-ui-sdl,-        tasty-bench >=0.3 && <0.6+        tasty-bench ^>=0.4     if os(windows)-        build-depends: Win32 >=2.13 && <2.15+        build-depends: Win32 ^>=2.14     hs-source-dirs:   benchmark     if os(windows)         ghc-options: -optl-mconsole