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 +56/−7
- cbits/nano_ui_display.c +5/−1
- data/inter-LICENSE.txt +0/−92
- lib/NanoUI/Backend/Sdl.hs +2/−1
- lib/NanoUI/Sdl/Display.hs +31/−5
- lib/NanoUI/Sdl/Font.hs +5/−5
- lib/NanoUI/Sdl/Image.hs +3/−4
- lib/NanoUI/Sdl/Input.hs +34/−18
- lib/NanoUI/Sdl/Runner.hs +48/−14
- lib/NanoUI/Sdl/Window.hs +75/−19
- nano-ui-sdl.cabal +29/−38
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