From b35539d9181bbef06da4fe7f61e55cad822df756 Mon Sep 17 00:00:00 2001

From: =?UTF-8?q?Jaakko=20Kera=CC=88nen?= jaakko.keranen@iki.fi

Date: Sat, 27 Feb 2021 12:28:24 +0200

Subject: [PATCH 1/1] Merged changes for v1.2 from dev branch


.gitignore | 3 +-

CMakeLists.txt | 62 +-

Depends-iOS.cmake | 21 +

Depends.cmake | 20 +

res/LaunchScreen.storyboard | 7 +

res/MacOSXBundleInfo.plist.in | 15 +

res/about/help.gmi | 43 +-

res/about/version.gmi | 49 +-

res/fi.skyjake.Lagrange.appdata.xml | 20 +

src/app.c | 340 +++-

src/app.h | 11 +

src/audio/player.c | 11 +-

src/bookmarks.c | 39 +-

src/bookmarks.h | 1 +

src/feeds.c | 5 +-

src/gmdocument.c | 117 +-

src/gmdocument.h | 11 +-

src/gmrequest.c | 21 +-

src/gmrequest.h | 1 +

src/gmutil.c | 51 +-

src/gmutil.h | 3 +

src/gopher.c | 14 +-

src/history.c | 12 +

src/history.h | 1 +

src/ios.h | 33 +

src/ios.m | 105 ++

src/main.c | 15 +-

src/media.c | 183 +-

src/media.h | 29 +-

src/mimehooks.c | 125 +-

src/prefs.c | 8 +

src/prefs.h | 4 +

src/stb_image_resize.h | 2631 +++++++++++++++++++++++++++

src/ui/bindingswidget.c | 4 +-

src/ui/color.c | 213 ++-

src/ui/color.h | 10 +

src/ui/documentwidget.c | 720 ++++----

src/ui/documentwidget.h | 1 +

src/ui/indicatorwidget.c | 1 +

src/ui/inputwidget.c | 123 +-

src/ui/inputwidget.h | 1 +

src/ui/keys.c | 4 +

src/ui/labelwidget.c | 47 +-

src/ui/labelwidget.h | 1 +

src/ui/listwidget.c | 20 +-

src/ui/lookupwidget.c | 22 +-

src/ui/{playerui.c => mediaui.c} | 111 +-

src/ui/{playerui.h => mediaui.h} | 17 +

src/ui/scrollwidget.c | 4 +-

src/ui/sidebarwidget.c | 199 +-

src/ui/sidebarwidget.h | 11 +-

src/ui/text.c | 99 +-

src/ui/text.h | 12 +-

src/ui/touch.c | 413 +++++

src/ui/touch.h | 32 +

src/ui/util.c | 372 ++--

src/ui/util.h | 5 +-

src/ui/widget.c | 167 +-

src/ui/widget.h | 33 +-

src/ui/window.c | 962 +++++++++-

src/ui/window.h | 39 +-

src/win32.c | 133 ++

src/win32.h | 11 +

63 files changed, 6753 insertions(+), 1045 deletions(-)

create mode 100644 Depends-iOS.cmake

create mode 100644 Depends.cmake

create mode 100644 res/LaunchScreen.storyboard

create mode 100644 src/ios.h

create mode 100644 src/ios.m

create mode 100644 src/stb_image_resize.h

rename src/ui/{playerui.c => mediaui.c} (67%)

rename src/ui/{playerui.h => mediaui.h} (76%)

create mode 100644 src/ui/touch.c

create mode 100644 src/ui/touch.h

diff --git a/.gitignore b/.gitignore

index 05eba91e..9ef6983d 100644

--- a/.gitignore

+++ b/.gitignore

@@ -4,5 +4,4 @@

build-*

/.vsbuild

/.vscode

+/app

diff --git a/CMakeLists.txt b/CMakeLists.txt

index 9489f5a9..e13fc2d5 100644

--- a/CMakeLists.txt

+++ b/CMakeLists.txt

@@ -18,7 +18,7 @@

cmake_minimum_required (VERSION 3.9)

project (Lagrange

 DESCRIPTION "A Beautiful Gemini Client"

 LANGUAGES C

)

@@ -33,24 +33,11 @@ option (ENABLE_RELATIVE_EMBED "Resources should always be found via relative p

option (ENABLE_WINDOWPOS_FIX "Set position after showing window (workaround for SDL bug)" OFF)

option (ENABLE_IDLE_SLEEP "While idle, sleep in the main thread instead of waiting for events" ON)

option (ENABLE_DOWNLOAD_EDIT "Allow changing the Downloads directory" ON)

+option (ENABLE_CUSTOM_FRAME "Draw a custom window frame (Windows)" OFF)

include (BuildType.cmake)

include (res/Embed.cmake)

-if (NOT EXISTS ${CMAKE_SOURCE_DIR}/lib/the_Foundation/CMakeLists.txt)

-else ()

-endif ()

-find_package (PkgConfig REQUIRED)

-pkg_check_modules (SDL2 REQUIRED sdl2)

-pkg_check_modules (MPG123 IMPORTED_TARGET libmpg123)

+include (Depends.cmake)

Embedded resources are written to a generated source file.

message (STATUS "Preparing embedded resources...")

@@ -83,7 +70,7 @@ set (EMBED_RESOURCES

 res/fonts/SourceSansPro-Bold.ttf

 res/fonts/Symbola.ttf

)

-if (UNIX AND NOT APPLE)

+if ((UNIX AND NOT APPLE) OR MSYS)

 list (APPEND EMBED_RESOURCES res/lagrange-64.png)

endif ()

embed_make (${EMBED_RESOURCES})

@@ -122,6 +109,7 @@ set (SOURCES

 src/prefs.c

 src/prefs.h

 src/stb_image.h

 src/stb_truetype.h

 src/visited.c

 src/visited.h

@@ -154,14 +142,16 @@ set (SOURCES

 src/ui/metrics.h

 src/ui/paint.c

 src/ui/paint.h

 src/ui/scrollwidget.c

 src/ui/scrollwidget.h

 src/ui/sidebarwidget.c

 src/ui/sidebarwidget.h

 src/ui/text.c

 src/ui/text.h

 src/ui/util.c

 src/ui/util.h

 src/ui/visbuf.c

@@ -184,7 +174,18 @@ set (SOURCES

 ${CMAKE_CURRENT_BINARY_DIR}/embedded.h

)

if (IOS)

elseif (APPLE)

 list (APPEND SOURCES src/macos.m src/macos.h)

 list (APPEND RESOURCES "res/Lagrange.icns")

endif ()

@@ -231,6 +232,9 @@ endif ()

if (ENABLE_DOWNLOAD_EDIT)

 target_compile_definitions (app PUBLIC LAGRANGE_DOWNLOAD_EDIT=1)

endif ()

+if (ENABLE_CUSTOM_FRAME AND MSYS)

+endif ()

target_link_libraries (app PUBLIC the_Foundation::the_Foundation)

target_link_libraries (app PUBLIC ${SDL2_LDFLAGS})

if (APPLE)

@@ -239,13 +243,15 @@ if (APPLE)

 else ()

     target_link_libraries (app PUBLIC "-framework AppKit")

 endif ()

     target_compile_options (app PUBLIC -mmacosx-version-min=${CMAKE_OSX_DEPLOYMENT_TARGET})

     target_link_options (app PUBLIC -mmacosx-version-min=${CMAKE_OSX_DEPLOYMENT_TARGET})

 endif ()

 set_target_properties (app PROPERTIES

     OUTPUT_NAME "Lagrange"

     MACOSX_BUNDLE YES

     MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_LIST_DIR}/res/MacOSXBundleInfo.plist.in"

     MACOSX_BUNDLE_BUNDLE_NAME "Lagrange"

@@ -258,9 +264,21 @@ if (APPLE)

     MACOSX_BUNDLE_COPYRIGHT "© ${COPYRIGHT_YEAR} Jaakko Keränen"

     XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER "fi.skyjake.Lagrange"

 )

endif ()

if (MSYS)

endif ()

if (UNIX)

 target_link_libraries (app PUBLIC m)

diff --git a/Depends-iOS.cmake b/Depends-iOS.cmake

new file mode 100644

index 00000000..013ee09a

--- /dev/null

+++ b/Depends-iOS.cmake

@@ -0,0 +1,21 @@

+message (STATUS "iOS dependency directory: ${IOS_DIR}")

+find_package (the_Foundation REQUIRED)

+set (SDL2_INCLUDE_DIRS ${IOS_DIR}/include/SDL2)

+set (SDL2_LDFLAGS

+)

diff --git a/Depends.cmake b/Depends.cmake

new file mode 100644

index 00000000..b4dacf7c

--- /dev/null

+++ b/Depends.cmake

@@ -0,0 +1,20 @@

+if (IOS)

+endif ()

+if (NOT EXISTS ${CMAKE_SOURCE_DIR}/lib/the_Foundation/CMakeLists.txt)

+else ()

+endif ()

+find_package (PkgConfig REQUIRED)

+pkg_check_modules (SDL2 REQUIRED sdl2)

+pkg_check_modules (MPG123 IMPORTED_TARGET libmpg123)

diff --git a/res/LaunchScreen.storyboard b/res/LaunchScreen.storyboard

new file mode 100644

index 00000000..f9a048ed

--- /dev/null

+++ b/res/LaunchScreen.storyboard

@@ -0,0 +1,7 @@

+

+

+

diff --git a/res/MacOSXBundleInfo.plist.in b/res/MacOSXBundleInfo.plist.in

index 1d769768..186e733b 100644

--- a/res/MacOSXBundleInfo.plist.in

+++ b/res/MacOSXBundleInfo.plist.in

@@ -34,6 +34,21 @@

<true/>

<key>NSRequiresAquaSystemAppearance</key>

<false/>

<key>CFBundleDocumentTypes</key>

<array>

	<dict>

diff --git a/res/about/help.gmi b/res/about/help.gmi

index fcdc8239..15485571 100644

--- a/res/about/help.gmi

+++ b/res/about/help.gmi

@@ -6,6 +6,13 @@

Help

+## What is Gemini

+Gemini is a simple protocol for serving content over the internet. It specifies a Markdown inspired format allowing basic plain text document markup. Compared to HTTP and HTML, Gemini is vastly simpler and easier to work with.

+=> gemini://gemini.circumlunar.space/docs/faq.gmi Project Gemini FAQ

+=> gemini://gemini.circumlunar.space/docs/specification.gmi Protocol and 'text/gemini' specification

What is Lagrange

Lagrange is a GUI client for browsing Geminispace. It offers modern conveniences familiar from web browsers, such as smooth scrolling, inline image viewing, multiple tabs, visual themes, Unicode fonts, bookmarks, history, and page outlines.

@@ -24,11 +31,12 @@ Like Gemini, Lagrange has been designed with minimalism in mind. It depends on a

-* Subscribe to Gemini feeds

+* Subscribe to Gemini and Atom feeds

+* Search engine integration

@@ -41,13 +49,6 @@ Like Gemini, Lagrange has been designed with minimalism in mind. It depends on a

-## What is Gemini

-Gemini is a simple protocol for serving content over the internet. It specifies a Markdown inspired format allowing basic plain text document markup. Compared to HTTP and HTML, Gemini is vastly simpler and easier to work with.

-=> gemini://gemini.circumlunar.space/docs/faq.gmi Project Gemini FAQ

-=> gemini://gemini.circumlunar.space/docs/specification.gmi Protocol and 'text/gemini' specification

Why not just use a web browser

Modern web browsers are complex beasts. In fact, they are so complex that one can create a fully functional virtual machine inside one and run another operating system!

@@ -88,6 +89,8 @@ Search within cached pages is limited to the (small) set of pages that Lagrange

Note that the navigation stack is saved to a file when Lagrange is shut down and restored on the next launch. This means the next time you launch Lagrange, you can still search the contents of past pages. However, navigation stacks are tab-specific, so closing a tab will delete its history as well.

+You can also make online search queries via the URL input field. When a search URL is configured on the "Network" tab of Preferences, text entered in the URL field is passed to the search URL as a query parameter. A search query will only occur when Enter is pressed while the [⇒ Search Query] indicator is visible.

1.1.2 Links

The type and destination of a link are indicated by the link's icon and color: ➤ links to the same domain, and 🌐 to a different domain. The colors are:

@@ -127,7 +130,13 @@ You can give it a try now with the link below. Either hold down ${ALT}, or press

Press ${CTRL+}T to open a new tab, and ${CTRL+}W to close the current tab. Right-clicking on buttons in the tab bar shows a context menu for additional tab-related functions.

-The set of open tabs is restored when you launch Lagrange.

+The set of open tabs and their full contents are saved when you quit the application and restored when relaunch it.

+### 1.2.1 Auto-reloading

+A tab can be set to auto-reload at given intervals. The setting is remembered until the tab is closed. This is helpful if you keep a periodically updated page open for longer periods of time.

+The feature can be found in the page context menu: right-click and select "Set Auto-Reload...".

1.3 Sidebars

@@ -149,6 +158,8 @@ Bookmarks are listed in alphabetic order in the sidebar. There is no support for

In addition to a title, bookmarks can have tags. Some tags have a special meaning, but you are free to enter whatever you want in the tags field. In quick search results, tags are given extra weight so they appear higher in results.

+By default bookmarks are assigned random icons. You can enter a custom icon for a bookmark in the bookmark edit dialog to make it easier to recognize a particular site at a glance. The icon must be a single Unicode character. It will appear in the bookmark list, tab titles, feed entry list, quick search results, and page top banners.

1.4.1 Exporting and importing

=> about:bookmarks The special page "about:bookmarks" is used for exporting bookmarks out of Lagrange.

@@ -179,13 +190,15 @@ Note that remote bookmarks are read-only: they cannot be edited or tagged. This

+* The "usericon" tag prevents a random icon to be selected for the bookmark. This tag is automatically applied when an icon character is entered in the bookmark editor.

1.5 Subscribing to feeds

-You may be familiar with RSS and Atom XML feeds from the web. Lagrange does not support RSS or Atom, only Gemini feeds. A Gemini feed is simply a regular 'text/gemini' page that contains one or more links whose labels are formatted in a particular way.

+You may be familiar with XML-based RSS and Atom feeds from the web. The Gemini equivalent of these is Gemini feeds. A Gemini feed is simply a regular 'text/gemini' page that contains one or more links whose labels are formatted in a particular way. This makes it very easy to write pages that clients can subscribe to.

=> gemini://gemini.circumlunar.space/docs/companion/subscription.gmi See "Subscribing to Gemini pages" for more information.

+Lagrange supports Gemini and Atom feed subscriptions. Atom feeds are automatically translated to the Gemini feed format so they can be viewed and subscribed to like a normal 'text/gemini' page. RSS feeds are not supported.

Subscriptions are managed via bookmarks. When you subscribe to a feed page, a bookmark is created and the special "subscribed" tag is applied on it. In the Bookmarks list, this is indicated by a ★ icon. There is no other difference between normal bookmarks and feed subscriptions — you may tag any bookmark as "subscribed" and Lagrange will look through it for feed-style links. The bookmark title is used as the feed title. This defaults to the top heading of the feed index page, but you can edit it to suit your needs.

Feeds are refreshed periodically while Lagrange is running, and also immediately after launching if it has been a while since the previous refresh. You may also manually refresh all feeds via the menus or by pressing ${SHIFT+}${CTRL+}R.

@@ -259,6 +272,8 @@ There are four different color schemes for the UI: two for dark mode, and two fo

On macOS, Lagrange will automatically switch between dark and light modes if the "Use system theme" setting is enabled. On other platforms you'll need to switch manually.

+On Windows, an option is provided to use a custom window frame. This provides a more consistent visual style for the application. However, the custom frame overrides default behaviors of the window, so if you are a Windows power user expect it to not support all the special interactions. For example, when pressing the Windows+Left/Right key, the window is resized to take over one half of the screen. Normally, Windows would then prompt to select another window to fill the rest of the screen, but this does not happen with the custom window frame.

Options for wide window sizes:

@@ -281,12 +296,14 @@ The "Style" tab of the Preferences dialog lets you customize the appearance and

The fonts for headings and other text are selected separately. This way one can achieve a greater number of style variations. Also, headings that are in a different font are more visually distinct and thus easier for one's eyes to scan.

-There are two sans-serif and two serif fonts:

+There are six fonts available:

+* "Source Sans Pro" is the Lagrange UI font.

+* "Iosevka" is the monospace font.

Other style options:

@@ -387,7 +404,7 @@ Any output that does not follow this format is considered to mean that the hook

4.2 mimehooks.txt syntax

Like other Lagrange configuration lines, mimehooks.txt has a simple line-oriented syntax. Lagrange must be restarted for changes to the configuration file to take effect.

+≈

Each hook is specified as three lines:

diff --git a/res/about/version.gmi b/res/about/version.gmi

index 7a08503b..8bd30dfb 100644

--- a/res/about/version.gmi

+++ b/res/about/version.gmi

@@ -6,6 +6,49 @@

Release notes

+## 1.2

+New features:

+* Atom feed subscriptions: Atom XML documents are automatically converted to Gemini feed index pages. This is a built-in version of the Atom-to-Gemini example on the Help page.

+* Inline downloads: right-click on any link that is openable inside Lagrange and select "Download Linked File".

+* Editable bookmark icons: use your favorite Unicode character as the symbol to represent a bookmarked site.

+* Searching via URL field: non-URL text entered in the field is passed onto the configured search query URL (Preferences > Network). An indicator is shown if a query will take place.

+* Tab auto-reloading: configure a reloading interval using the page context menu ("Set Auto-Reload..."). Auto-reloading is part of the persistent state of the tab.

+* "Iosevka" and "Source Sans Pro" (the UI font) can be used as heading and body fonts.

+* User preference for aligning all pages to the top of the window.

+* Keybinding (F11) for toggling fullscreen mode. On macOS, the shortcut is ⌃⌘F as before.

+* Keybinding for finding text on page.

+UI design:

+* Enhanced navbar: adjusted spacing, URL field has a maximum width, tab titles have less pronounced borders.

+* Improved sidebar appearance: bold subheadings, larger feed icons, adjusted spacing, background color.

+* Font consistency: all UI elements use the same font (i.e., no more monospace input fields).

+* Added setting for UI accent color (teal, orange).

+* General fine-tuning of the color palette.

+* Dialog buttons are aligned to the right edge, leaving room for additional action buttons on the left.

+* Page Information button is embedded in the URL field.

+* Page Information dialog is attached to its button.

+* Site icons use a different color in tab titles for visual distinction.

+* Fade background behind modal dialogs.

+* Responsive page margins.

+* Windows: Added a custom window frame to replace the default Windows one. This looks nicer but does not behave exactly like a native window frame. Added a setting to Preferences for switching back to the default frame.

+Other changes:

+* Help is opened on first run instead of the "About Lagrange" page to make it easier to discover important Gemini links like the FAQ.

+* "Go to Root" respects a user name found in the URL. One can still "Go to Parent" to get above the user level.

+* Feed entries are sorted by refresh time if they are published on the same date.

+* Don't show future-dated feed entries in Feeds.

+* Middle-clicking on links: open new tab in background or foreground depending on the Shift key.

+* Shift+Insert can be used for pasting clipboard contents into input fields.

+* Removed a strange violet-on-green color theme pairing.

+Bug fixes:

+* Fixed text prompt dialogs closing and accepting the entered text when switching focus away from the app.

+* Scroll position remains fixed while horizontally resizing the window or sidebars.

+* Fixed a crash when opening the audio player menu.

+* Fixed Gopher requests that were using URL (percent) encoded characters.

+* Windows: Fixed a flash of white when the window is first opened.

1.1.4

@@ -251,7 +294,6 @@

0.5

-=> https://mpg123.org/ mpg123: MPEG audio player and decoder library

@@ -261,6 +303,7 @@

+=> https://mpg123.org/ mpg123: MPEG audio player and decoder library

0.4.1

@@ -272,8 +315,7 @@

0.4

-* Added audio playback with support for streaming. Supported audio formats in this release are WAV (PCM, mono/stereo, 8/16/24/32 integer/float) and Ogg Vorbis. Shoutout to Sean Barrett et al. for stb_vorbis:

-=> https://github.com/nothings/stb stb: single-file public domain libraries for C/C++

+* Added audio playback with support for streaming. Supported audio formats in this release are WAV (PCM, mono/stereo, 8/16/24/32 integer/float) and Ogg Vorbis. Shoutout to Sean Barrett et al. for stb_vorbis.

@@ -281,6 +323,7 @@

+=> https://github.com/nothings/stb stb: single-file public domain libraries for C/C++

0.3

diff --git a/res/fi.skyjake.Lagrange.appdata.xml b/res/fi.skyjake.Lagrange.appdata.xml

index 689a609a..355bf18d 100644

--- a/res/fi.skyjake.Lagrange.appdata.xml

+++ b/res/fi.skyjake.Lagrange.appdata.xml

@@ -45,6 +45,26 @@

 <update_contact>jaakko.keranen@iki.fi</update_contact>

 <releases>

     <release version="1.1.4" date="2021-02-22">

         <description>

             <p>Bug fixes:</p>

diff --git a/src/app.c b/src/app.c

index f5833c95..17b51dd4 100644

--- a/src/app.c

+++ b/src/app.c

@@ -61,9 +61,12 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */

#include <stdarg.h>

#include <errno.h>

-#if defined (iPlatformApple) && !defined (iPlatformIOS)

+#if defined (iPlatformAppleDesktop)

include "macos.h"

#endif

+#if defined (iPlatformAppleMobile)

+# include "ios.h"

+#endif

#if defined (iPlatformMsys)

include "win32.h"

#endif

@@ -73,10 +76,14 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */

iDeclareType(App)

-#if defined (iPlatformApple)

+#if defined (iPlatformAppleDesktop)

#define EMB_BIN "../../Resources/resources.lgr"

static const char *defaultDataDir_App_ = "~/Library/Application Support/fi.skyjake.Lagrange";

#endif

+#if defined (iPlatformAppleMobile)

+#define EMB_BIN "../../Resources/resources.lgr"

+static const char *defaultDataDir_App_ = "~/Library/Application Support";

+#endif

#if defined (iPlatformMsys)

#define EMB_BIN "../resources.lgr"

static const char *defaultDataDir_App_ = "~/AppData/Roaming/fi.skyjake.Lagrange";

@@ -119,6 +126,7 @@ struct Impl_App {

 iStringList *launchCommands;

 iBool        isFinishedLaunching;

 iTime        lastDropTime; /* for detecting drops of multiple items */

 /* Preferences: */

 iBool        commandEcho;         /* --echo */

 iBool        forceSoftwareRender; /* --sw */

@@ -154,33 +162,45 @@ static iString *serializePrefs_App_(const iApp *d) {

 iString *str = new_String();

 const iSidebarWidget *sidebar  = findWidget_App("sidebar");

 const iSidebarWidget *sidebar2 = findWidget_App("sidebar2");

+#if defined (LAGRANGE_CUSTOM_FRAME)

+#endif

 appendFormat_String(str, "window.retain arg:%d\n", d->prefs.retainWindowSize);

 if (d->prefs.retainWindowSize) {

     int w, h, x, y;

     appendFormat_String(str, "window.setrect width:%d height:%d coord:%d %d\n", w, h, x, y);

     appendFormat_String(str, "sidebar.width arg:%d\n", width_SidebarWidget(sidebar));

     appendFormat_String(str, "sidebar2.width arg:%d\n", width_SidebarWidget(sidebar2));

     /* On macOS, maximization should be applied at creation time or the window will take

        a moment to animate to its maximized size. */

-#if !defined (iPlatformApple)

+#if defined (LAGRANGE_CUSTOM_FRAME)

+#elif !defined (iPlatformApple)

         appendFormat_String(str, "~window.maximize\n");

     }

-#else

#endif

 }

 /* Sidebars. */ {

         appendCStr_String(str, "sidebar.toggle\n");

     }

     appendFormat_String(str, "sidebar.mode arg:%d\n", mode_SidebarWidget(sidebar));

         appendCStr_String(str, "sidebar2.toggle\n");

     }

     appendFormat_String(str, "sidebar2.mode arg:%d\n", mode_SidebarWidget(sidebar2));

@@ -199,9 +219,11 @@ static iString *serializePrefs_App_(const iApp *d) {

 appendFormat_String(str, "linewidth.set arg:%d\n", d->prefs.lineWidth);

 appendFormat_String(str, "prefs.biglede.changed arg:%d\n", d->prefs.bigFirstParagraph);

 appendFormat_String(str, "prefs.sideicon.changed arg:%d\n", d->prefs.sideIcon);

 appendFormat_String(str, "quoteicon.set arg:%d\n", d->prefs.quoteIcon ? 1 : 0);

 appendFormat_String(str, "prefs.hoverlink.changed arg:%d\n", d->prefs.hoverLink);

 appendFormat_String(str, "theme.set arg:%d auto:1\n", d->prefs.theme);

 appendFormat_String(str, "ostheme arg:%d\n", d->prefs.useSystemTheme);

 appendFormat_String(str, "doctheme.dark.set arg:%d\n", d->prefs.docThemeDark);

 appendFormat_String(str, "doctheme.light.set arg:%d\n", d->prefs.docThemeLight);

@@ -210,6 +232,7 @@ static iString *serializePrefs_App_(const iApp *d) {

 appendFormat_String(str, "proxy.gopher address:%s\n", cstr_String(&d->prefs.gopherProxy));

 appendFormat_String(str, "proxy.http address:%s\n", cstr_String(&d->prefs.httpProxy));

 appendFormat_String(str, "downloads path:%s\n", cstr_String(&d->prefs.downloadDir));

 return str;

}

@@ -270,7 +293,10 @@ static void loadPrefs_App_(iApp *d) {

         if (equal_Command(cmd, "uiscale")) {

             setUiScale_Window(get_Window(), argf_Command(cmd));

         }

             const iInt2 pos = coord_Command(cmd);

             d->initialWindowRect = init_Rect(

                 pos.x, pos.y, argLabel_Command(cmd, "width"), argLabel_Command(cmd, "height"));

@@ -285,6 +311,9 @@ static void loadPrefs_App_(iApp *d) {

 else {

     /* default preference values */

 }

+#if !defined (LAGRANGE_CUSTOM_FRAME)

+#endif

 iRelease(f);

}

@@ -370,6 +399,9 @@ static void saveState_App_(const iApp *d) {

         serializeState_DocumentWidget(i.object, stream_File(f));

     }

 }

 iRelease(f);

}

@@ -384,6 +416,12 @@ static uint32_t checkAsleep_App_(uint32_t interval, void *param) {

}

#endif

+static uint32_t postAutoReloadCommand_App_(uint32_t interval, void *param) {

+}

static void init_App_(iApp *d, int argc, char **argv) {

 init_CommandLine(&d->args, argc, argv);

 /* Where was the app started from? We ask SDL first because the command line alone is

@@ -438,8 +476,11 @@ static void init_App_(iApp *d, int argc, char **argv) {

 d->lastEventTime = 0;

 d->sleepTimer    = SDL_AddTimer(1000, checkAsleep_App_, d);

#endif

-#if defined (iPlatformApple)

+#if defined (iPlatformAppleDesktop)

 setupApplication_MacOS();

+#if defined (iPlatformAppleMobile)

+#endif

#endif

 init_Keys();

 loadPrefs_App_(d);

@@ -476,9 +517,11 @@ static void init_App_(iApp *d, int argc, char **argv) {

 /* Widget state init. */

 processEvents_App(postedEventsOnly_AppEventMode);

 if (!loadState_App_(d)) {

 }

 postCommand_App("window.unfreeze");

 d->isFinishedLaunching = iTrue;

 /* Run any commands that were pending completion of launch. */ {

     iForEach(StringList, i, d->launchCommands) {

@@ -539,6 +582,61 @@ const iString *downloadDir_App(void) {

 return collect_String(cleaned_Path(&app_.prefs.downloadDir));

}

+const iString *downloadPathForUrl_App(const iString *url, const iString *mime) {

+}

const iString *debugInfo_App(void) {

 extern char **environ; /* The environment variables. */

 iApp *d = &app_;

@@ -570,6 +668,39 @@ const iString *debugInfo_App(void) {

 return msg;

}

+static void clearCache_App_(void) {

+}

+void trimCache_App(void) {

+}

iLocalDef iBool isWaitingAllowed_App_(iApp *d) {

#if defined (LAGRANGE_IDLE_SLEEP)

 if (d->isIdling) {

@@ -587,10 +718,31 @@ void processEvents_App(enum iAppEventMode eventMode) {

         SDL_WaitEvent(&ev)) ||

        ((!isWaitingAllowed_App_(d) || eventMode == postedEventsOnly_AppEventMode) &&

         SDL_PollEvent(&ev))) {

+#if defined (iPlatformAppleMobile)

+#endif

     switch (ev.type) {

         case SDL_QUIT:

             d->isRunning = iFalse;

             goto backToMainLoop;

         case SDL_DROPFILE: {

             iBool wasUsed = processEvent_Window(d->window, &ev);

             if (!wasUsed) {

@@ -637,7 +789,7 @@ void processEvents_App(enum iAppEventMode eventMode) {

                 wasUsed = processEvent_Keys(&ev);

             }

             if (ev.type == SDL_USEREVENT && ev.user.code == command_UserEventCode) {

-#if defined (iPlatformApple) && !defined (iPlatformIOS)

+#if defined (iPlatformAppleDesktop)

                 handleCommand_MacOS(command_UserEvent(&ev));

#endif

                 if (isCommand_UserEvent(&ev, "metrics.changed")) {

@@ -857,6 +1009,20 @@ iMimeHooks *mimeHooks_App(void) {

 return app_.mimehooks;

}

+iBool isLandscape_App(void) {

+}

+enum iAppDeviceType deviceType_App(void) {

+#if defined (iPlatformAppleMobile)

+#else

+#endif

+}

iGmCerts *certs_App(void) {

 return app_.certs;

}

@@ -875,20 +1041,34 @@ static void updatePrefsThemeButtons_(iWidget *d) {

                     selected_WidgetFlag,

                     colorTheme_App() == i);

 }

}

-static void updateColorThemeButton_(iLabelWidget *button, int theme) {

+static void updateDropdownSelection_(iLabelWidget *dropButton, const char *selectedCommand) {

     iLabelWidget *item = i.object;

     }

 }

}

+static void updateColorThemeButton_(iLabelWidget *button, int theme) {

+// const char *mode = strstr(cstr_String(id_Widget(as_Widget(button))), ".dark")

+// ? "dark" : "light";

+}

+static void updateFontButton_(iLabelWidget *button, int font) {

+}

static iBool handlePrefsCommands_(iWidget *d, const char *cmd) {

 if (equal_Command(cmd, "prefs.dismiss") || equal_Command(cmd, "preferences")) {

     setUiScale_Window(get_Window(),

@@ -897,6 +1077,8 @@ static iBool handlePrefsCommands_(iWidget *d, const char *cmd) {

     postCommandf_App("downloads path:%s",

                      cstr_String(text_InputWidget(findChild_Widget(d, "prefs.downloads"))));

#endif

     postCommandf_App("window.retain arg:%d",

                      isSelected_Widget(findChild_Widget(d, "prefs.retainwindow")));

     postCommandf_App("smoothscroll arg:%d",

@@ -907,6 +1089,8 @@ static iBool handlePrefsCommands_(iWidget *d, const char *cmd) {

                      isSelected_Widget(findChild_Widget(d, "prefs.ostheme")));

     postCommandf_App("decodeurls arg:%d",

                      isSelected_Widget(findChild_Widget(d, "prefs.decodeurls")));

     postCommandf_App("cachesize.set arg:%d",

                      toInt_String(text_InputWidget(findChild_Widget(d, "prefs.cachesize"))));

     postCommandf_App("proxy.gemini address:%s",

@@ -919,6 +1103,7 @@ static iBool handlePrefsCommands_(iWidget *d, const char *cmd) {

     postCommandf_App("prefs.dialogtab arg:%u",

                      tabPageIndex_Widget(tabs, currentTabPage_Widget(tabs)));

     destroy_Widget(d);

     return iTrue;

 }

 else if (equal_Command(cmd, "quoteicon.set")) {

@@ -935,6 +1120,14 @@ static iBool handlePrefsCommands_(iWidget *d, const char *cmd) {

     updateColorThemeButton_(findChild_Widget(d, "prefs.doctheme.light"), arg_Command(cmd));

     return iFalse;

 }

 else if (equal_Command(cmd, "prefs.ostheme.changed")) {

     postCommandf_App("ostheme arg:%d", arg_Command(cmd));

 }

@@ -992,33 +1185,6 @@ iDocumentWidget *newTab_App(const iDocumentWidget *duplicateOf, iBool switchToNe

 return doc;

}

-void trimCache_App(void) {

-}

static iBool handleIdentityCreationCommands_(iWidget *dlg, const char *cmd) {

 iApp *d = &app_;

 if (equal_Command(cmd, "ident.temp.changed")) {

@@ -1092,6 +1258,15 @@ iBool willUseProxy_App(const iRangecc scheme) {

 return schemeProxy_App(scheme) != NULL;

}

+const iString *searchQueryUrl_App(const iString *queryStringUnescaped) {

+}

iBool handleCommand_App(const char *cmd) {

 iApp *d = &app_;

 if (equal_Command(cmd, "config.error")) {

@@ -1100,6 +1275,10 @@ iBool handleCommand_App(const char *cmd) {

                                    suffixPtr_Command(cmd, "where")));

     return iTrue;

 }

 else if (equal_Command(cmd, "prefs.dialogtab")) {

     d->prefs.dialogTab = arg_Command(cmd);

     return iTrue;

@@ -1108,8 +1287,24 @@ iBool handleCommand_App(const char *cmd) {

     d->prefs.retainWindowSize = arg_Command(cmd);

     return iTrue;

 }

 else if (equal_Command(cmd, "window.maximize")) {

     return iTrue;

 }

 else if (equal_Command(cmd, "font.set")) {

@@ -1170,6 +1365,12 @@ iBool handleCommand_App(const char *cmd) {

     postCommandf_App("theme.changed auto:%d", isAuto);

     return iTrue;

 }

 else if (equal_Command(cmd, "ostheme")) {

     d->prefs.useSystemTheme = arg_Command(cmd);

     return iTrue;

@@ -1219,6 +1420,11 @@ iBool handleCommand_App(const char *cmd) {

     postRefresh_App();

     return iTrue;

 }

 else if (equal_Command(cmd, "prefs.hoverlink.changed")) {

     d->prefs.hoverLink = arg_Command(cmd) != 0;

     postRefresh_App();

@@ -1241,6 +1447,17 @@ iBool handleCommand_App(const char *cmd) {

     }

     return iTrue;

 }

 else if (equal_Command(cmd, "proxy.gemini")) {

     setCStr_String(&d->prefs.geminiProxy, suffixPtr_Command(cmd, "address"));

     return iTrue;

@@ -1332,7 +1549,13 @@ iBool handleCommand_App(const char *cmd) {

     return iTrue;

 }

 else if (equal_Command(cmd, "tabs.close")) {

+#if defined (iPlatformAppleMobile)

+#endif

     const iRangecc tabId = range_Command(cmd, "id");

     iWidget *      doc   = !isEmpty_Range(&tabId) ? findWidget_App(cstr_Rangecc(tabId))

                                                   : document_App();

@@ -1385,6 +1608,7 @@ iBool handleCommand_App(const char *cmd) {

     setToggle_Widget(findChild_Widget(dlg, "prefs.smoothscroll"), d->prefs.smoothScrolling);

     setToggle_Widget(findChild_Widget(dlg, "prefs.imageloadscroll"), d->prefs.loadImageInsteadOfScrolling);

     setToggle_Widget(findChild_Widget(dlg, "prefs.ostheme"), d->prefs.useSystemTheme);

     setToggle_Widget(findChild_Widget(dlg, "prefs.retainwindow"), d->prefs.retainWindowSize);

     setText_InputWidget(findChild_Widget(dlg, "prefs.uiscale"),

                         collectNewFormat_String("%g", uiScale_Window(d->window)));

@@ -1411,8 +1635,11 @@ iBool handleCommand_App(const char *cmd) {

         iTrue);

     setToggle_Widget(findChild_Widget(dlg, "prefs.biglede"), d->prefs.bigFirstParagraph);

     setToggle_Widget(findChild_Widget(dlg, "prefs.sideicon"), d->prefs.sideIcon);

     updateColorThemeButton_(findChild_Widget(dlg, "prefs.doctheme.dark"), d->prefs.docThemeDark);

     updateColorThemeButton_(findChild_Widget(dlg, "prefs.doctheme.light"), d->prefs.docThemeLight);

     setFlags_Widget(

         findChild_Widget(

             dlg, format_CStr("prefs.saturation.%d", (int) (d->prefs.saturation * 3.99f))),

@@ -1421,6 +1648,7 @@ iBool handleCommand_App(const char *cmd) {

     setText_InputWidget(findChild_Widget(dlg, "prefs.cachesize"),

                         collectNewFormat_String("%d", d->prefs.maxCacheSize));

     setToggle_Widget(findChild_Widget(dlg, "prefs.decodeurls"), d->prefs.decodeUserVisibleURLs);

     setText_InputWidget(findChild_Widget(dlg, "prefs.proxy.gemini"), &d->prefs.geminiProxy);

     setText_InputWidget(findChild_Widget(dlg, "prefs.proxy.gopher"), &d->prefs.gopherProxy);

     setText_InputWidget(findChild_Widget(dlg, "prefs.proxy.http"), &d->prefs.httpProxy);

@@ -1568,9 +1796,10 @@ void openInDefaultBrowser_App(const iString *url) {

     return;

 }

#endif

+#if !defined (iPlatformAppleMobile)

 iProcess *proc = new_Process();

 setArguments_Process(proc,

-#if defined (iPlatformApple)

+#if defined (iPlatformAppleDesktop)

                      iClob(newStringsCStr_StringList("/usr/bin/env", "open", cstr_String(url), NULL))

#elif defined (iPlatformLinux) || defined (iPlatformOther)

                      iClob(newStringsCStr_StringList("/usr/bin/env", "xdg-open", cstr_String(url), NULL))

@@ -1584,10 +1813,11 @@ void openInDefaultBrowser_App(const iString *url) {

 );

 start_Process(proc);

 iRelease(proc);

+#endif

}

void revealPath_App(const iString *path) {

-#if defined (iPlatformApple)

+#if defined (iPlatformAppleDesktop)

 const char *scriptPath = concatPath_CStr(dataDir_App_(), "revealfile.scpt");

 iFile *f = newCStr_File(scriptPath);

 if (open_File(f, writeOnly_FileMode | text_FileMode)) {

diff --git a/src/app.h b/src/app.h

index efaf0a3e..9a68c362 100644

--- a/src/app.h

+++ b/src/app.h

@@ -38,6 +38,12 @@ iDeclareType(MimeHooks)

iDeclareType(Visited)

iDeclareType(Window)

+enum iAppDeviceType {

+};

enum iAppEventMode {

 waitForNewEvents_AppEventMode,

 postedEventsOnly_AppEventMode,

@@ -61,6 +67,9 @@ void refresh_App (void);

iBool isRefreshPending_App (void);

uint32_t elapsedSinceLastTicker_App (void); /* milliseconds */

+iBool isLandscape_App (void);

+iLocalDef iBool isPortrait_App (void) { return !isLandscape_App(); }

+enum iAppDeviceType deviceType_App (void);

iGmCerts * certs_App (void);

iVisited * visited_App (void);

iBookmarks * bookmarks_App (void);

@@ -75,6 +84,8 @@ iBool forceSoftwareRender_App(void);

enum iColorTheme colorTheme_App (void);

const iString * schemeProxy_App (iRangecc scheme);

iBool willUseProxy_App (const iRangecc scheme);

+const iString * searchQueryUrl_App (const iString *queryStringUnescaped);

+const iString * downloadPathForUrl_App(const iString *url, const iString *mime);

typedef void (*iTickerFunc)(iAny *);

diff --git a/src/audio/player.c b/src/audio/player.c

index 1c8538b4..d2ec9870 100644

--- a/src/audio/player.c

+++ b/src/audio/player.c

@@ -566,6 +566,9 @@ static iContentSpec contentSpec_Player_(const iPlayer *d) {

     stb_vorbis *vrb = stb_vorbis_open_pushdata(

         constData_Block(&d->data->data), size_Block(&d->data->data), &consumed, &error, NULL);

     if (!vrb) {

         return content;

     }

     const stb_vorbis_info info = stb_vorbis_get_info(vrb);

@@ -793,8 +796,10 @@ iString *metadataLabel_Player(const iPlayer *d) {

     }

     unlock_Mutex(&d->decoder->tagMutex);

 }

 return meta;

}

diff --git a/src/bookmarks.c b/src/bookmarks.c

index 1fc24a67..91280f3c 100644

--- a/src/bookmarks.c

+++ b/src/bookmarks.c

@@ -65,8 +65,10 @@ void addTag_Bookmark(iBookmark *d, const char *tag) {

void removeTag_Bookmark(iBookmark *d, const char *tag) {

 const size_t pos = indexOfCStr_String(&d->tags, tag);

}

iDefineTypeConstruction(Bookmark)

@@ -237,7 +239,7 @@ iBool updateBookmarkIcon_Bookmarks(iBookmarks *d, const iString *url, iChar icon

 const uint32_t id = findUrl_Bookmarks(d, url);

 if (id) {

     iBookmark *bm = get_Bookmarks(d, id);

         if (icon != bm->icon) {

             bm->icon = icon;

             changed = iTrue;

@@ -248,6 +250,37 @@ iBool updateBookmarkIcon_Bookmarks(iBookmarks *d, const iString *url, iChar icon

 return changed;

}

+iChar siteIcon_Bookmarks(const iBookmarks *d, const iString *url) {

+}

iBookmark *get_Bookmarks(iBookmarks *d, uint32_t id) {

 return (iBookmark *) value_Hash(&d->bookmarks, id);

}

diff --git a/src/bookmarks.h b/src/bookmarks.h

index d5182b48..ab9c683b 100644

--- a/src/bookmarks.h

+++ b/src/bookmarks.h

@@ -62,6 +62,7 @@ iBookmark * get_Bookmarks (iBookmarks *, uint32_t id);

void fetchRemote_Bookmarks (iBookmarks *);

void requestFinished_Bookmarks (iBookmarks *, iGmRequest *req);

iBool updateBookmarkIcon_Bookmarks(iBookmarks *, const iString *url, iChar icon);

+iChar siteIcon_Bookmarks (const iBookmarks *, const iString *url);

void save_Bookmarks (const iBookmarks *, const char *dirPath);

uint32_t findUrl_Bookmarks (const iBookmarks *, const iString url); / O(n) */

diff --git a/src/feeds.c b/src/feeds.c

index 3fb05d14..c66b2b84 100644

--- a/src/feeds.c

+++ b/src/feeds.c

@@ -598,7 +598,10 @@ void removeEntries_Feeds(uint32_t feedBookmarkId) {

static int cmpTimeDescending_FeedEntryPtr_(const void *a, const void *b) {

 const iFeedEntry * const *e1 = a, * const *e2 = b;

}

const iPtrArray *listEntries_Feeds(void) {

diff --git a/src/gmdocument.c b/src/gmdocument.c

index f73b7dc4..4e76a22a 100644

--- a/src/gmdocument.c

+++ b/src/gmdocument.c

@@ -27,6 +27,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */

#include "ui/metrics.h"

#include "ui/window.h"

#include "visited.h"

+#include "bookmarks.h"

#include "app.h"

#include <the_Foundation/ptrarray.h>

@@ -254,6 +255,15 @@ static iBool isForcedMonospace_GmDocument_(const iGmDocument *d) {

 return iFalse;

}

+static void linkContentLaidOut_GmDocument_(iGmDocument *d, const iGmMediaInfo *mediaInfo,

+}

static void doLayout_GmDocument_(iGmDocument *d) {

 const iBool isMono = isForcedMonospace_GmDocument_(d);

 /* TODO: Collect these parameters into a GmTheme. */

@@ -281,10 +291,10 @@ static void doLayout_GmDocument_(iGmDocument *d) {

     5, 10, 5, 10, 0, 0, 0, 5

 };

 static const float topMargin[max_GmLineType] = {

 };

 static const float bottomMargin[max_GmLineType] = {

 };

 static const char *arrow           = "\u27a4";

 static const char *envelope        = "\U0001f4e7";

@@ -294,7 +304,6 @@ static void doLayout_GmDocument_(iGmDocument *d) {

 static const char *quote           = "\u201c";

 static const char *magnifyingGlass = "\U0001f50d";

 static const char *pointingFinger  = "\U0001f449";

 const iPrefs *prefs = prefs_App();

 clear_Array(&d->layout);

 clearLinks_GmDocument_(d);

@@ -325,6 +334,7 @@ static void doLayout_GmDocument_(iGmDocument *d) {

     iGmRun run = { .color = white_ColorId };

     enum iGmLineType type;

     int indent = 0;

     /* Detect the type of the line. */

     if (!isPreformat) {

         type = lineType_GmDocument_(d, line);

@@ -431,8 +441,7 @@ static void doLayout_GmDocument_(iGmDocument *d) {

         if ((type == link_GmLineType && prevType == link_GmLineType) ||

             (type == quote_GmLineType && prevType == quote_GmLineType)) {

             /* No margin between consecutive links/quote lines. */

         }

         if (isEmpty_Array(&d->layout)) {

             required = 0; /* top of document */

@@ -522,17 +531,13 @@ static void doLayout_GmDocument_(iGmDocument *d) {

     if (!prefs->quoteIcon && type == quote_GmLineType) {

         run.flags |= quoteBorder_GmRunFlag;

     }

     iAssert(!isEmpty_Range(&runLine)); /* must have something at this point */

     while (!isEmpty_Range(&runLine)) {

         run.bounds.pos = addX_I2(pos, indent * gap_Text);

         const char *contPos;

         const iInt2 dims  = tryAdvance_Text(run.font, runLine, avail, &contPos);

         iChangeFlags(run.flags, wide_GmRunFlag, (isPreformat && dims.x > d->size.x));

         run.bounds.size.x = iMax(avail, dims.x); /* Extends to the right edge for selection. */

@@ -562,48 +567,40 @@ static void doLayout_GmDocument_(iGmDocument *d) {

     if (type == link_GmLineType) {

         const iMediaId imageId = findLinkImage_Media(d->media, run.linkId);

         const iMediaId audioId = !imageId ? findLinkAudio_Media(d->media, run.linkId) : 0;

         if (imageId) {

             imageInfo_Media(d->media, imageId, &img);

             const int margin = lineHeight_Text(paragraph_FontId) / 2;

             pos.y += margin;

             run.bounds.pos = pos;

             run.bounds.size.x = d->size.x;

             run.bounds.size.y = d->size.x * aspect;

             run.visBounds = run.bounds;

             if (width_Rect(run.visBounds) > maxSize.x) {

                 /* Don't scale the image up. */

                 run.visBounds.size.x = maxSize.x;

             }

             pushBack_Array(&d->layout, &run);

             pos.y += run.bounds.size.y + margin;

         }

         else if (audioId) {

             audioInfo_Media(d->media, audioId, &info);

             const int margin = lineHeight_Text(paragraph_FontId) / 2;

             pos.y += margin;

             run.bounds.pos    = pos;

@@ -612,7 +609,25 @@ static void doLayout_GmDocument_(iGmDocument *d) {

             run.visBounds     = run.bounds;

             run.text          = iNullRange;

             run.color         = 0;

             pushBack_Array(&d->layout, &run);

             pos.y += run.bounds.size.y + margin;

         }

@@ -719,6 +734,13 @@ static void setDerivedThemeColors_(enum iGmDocumentTheme theme) {

 }

}

+static void updateIconBasedOnUrl_GmDocument_(iGmDocument *d) {

+}

void setThemeSeed_GmDocument(iGmDocument *d, const iBlock *seed) {

 const iPrefs *        prefs = prefs_App();

 enum iGmDocumentTheme theme =

@@ -803,8 +825,8 @@ void setThemeSeed_GmDocument(iGmDocument *d, const iBlock *seed) {

         set_Color(tmHeading2_ColorId, mix_Color(get_Color(tmBackground_ColorId), get_Color(black_ColorId), 0.67f));

         set_Color(tmHeading3_ColorId, mix_Color(get_Color(tmBackground_ColorId), get_Color(black_ColorId), 0.55f));

         setHsl_Color(tmBannerBackground_ColorId, addSatLum_HSLColor(base, 0, -0.1f));

         setHsl_Color(tmLinkIcon_ColorId, addSatLum_HSLColor(get_HSLColor(teal_ColorId), 0, 0));

         set_Color(tmLinkIconVisited_ColorId, mix_Color(get_Color(tmBackground_ColorId), get_Color(teal_ColorId), 0.35f));

         set_Color(tmLinkDomain_ColorId, get_Color(teal_ColorId));

@@ -914,7 +936,7 @@ void setThemeSeed_GmDocument(iGmDocument *d, const iBlock *seed) {

         violet_Hue,

         pink_Hue

     };

     static const struct {

         int index[2];

     } altHues[iElemCount(hues)] = {

@@ -922,7 +944,7 @@ void setThemeSeed_GmDocument(iGmDocument *d, const iBlock *seed) {

         { 8, 3 },  /* reddish orange */

         { 7, 9 },  /* yellowish orange */

         { 5, 7 },  /* yellow */

         { 1, 3 },  /* green */

         { 2, 4 },  /* bluish green */

         { 2, 11 }, /* cyan */

@@ -968,6 +990,8 @@ void setThemeSeed_GmDocument(iGmDocument *d, const iBlock *seed) {

         setHsl_Color(tmHeading2_ColorId, setLum_HSLColor(altBase, titleLum + 0.70f));

         setHsl_Color(tmHeading3_ColorId, setLum_HSLColor(altBase, titleLum + 0.60f));

+// printf("titleLum: %f\n", titleLum);

         setHsl_Color(tmParagraph_ColorId, addSatLum_HSLColor(base, 0.1f, 0.6f));

// printf("heading3: %d,%d,%d\n", get_Color(tmHeading3_ColorId).r, get_Color(tmHeading3_ColorId).g, get_Color(tmHeading3_ColorId).b);

@@ -976,11 +1000,8 @@ void setThemeSeed_GmDocument(iGmDocument *d, const iBlock *seed) {

         if (delta_Color(get_Color(tmHeading3_ColorId), get_Color(tmParagraph_ColorId)) <= 80) {

             /* Smallest headings may be too close to body text color. */

-// iHSLColor clr = get_HSLColor(tmParagraph_ColorId);

-// clr.lum = iMax(0.5f, clr.lum - 0.15f);

         }

         setHsl_Color(tmFirstParagraph_ColorId, addSatLum_HSLColor(base, 0.2f, 0.72f));

@@ -1091,6 +1112,7 @@ void setThemeSeed_GmDocument(iGmDocument *d, const iBlock *seed) {

     if (equal_CStr(cstr_Block(seed), "gemini.circumlunar.space")) {

         d->siteIcon = 0x264a; /* gemini symbol */

     }

 }

#if 0

 for (int i = tmFirst_ColorId; i < max_ColorId; ++i) {

@@ -1192,6 +1214,7 @@ void setUrl_GmDocument(iGmDocument *d, const iString *url) {

 iUrl parts;

 init_Url(&parts, url);

 setRange_String(&d->localHost, parts.host);

}

void setSource_GmDocument(iGmDocument *d, const iString *source, int width) {

diff --git a/src/gmdocument.h b/src/gmdocument.h

index e2c7e10c..16127ea3 100644

--- a/src/gmdocument.h

+++ b/src/gmdocument.h

@@ -85,17 +85,24 @@ enum iGmRunFlags {

 wide_GmRunFlag        = iBit(6), /* horizontally scrollable */

};

+enum iGmRunMediaType {

+};

struct Impl_GmRun {

 iRangecc  text;

 uint8_t   font;

 uint8_t   color;

 uint8_t   flags;

 iRect     bounds;    /* used for hit testing, may extend to edges */

 iRect     visBounds; /* actual visual bounds */

 uint16_t  preId;     /* preformatted block ID (sequential) */

 iGmLinkId linkId;    /* zero for non-links */

};

iDeclareType(GmRunRange)

diff --git a/src/gmrequest.c b/src/gmrequest.c

index 8626403f..0208dc94 100644

--- a/src/gmrequest.c

+++ b/src/gmrequest.c

@@ -133,6 +133,7 @@ struct Impl_GmRequest {

 iTlsRequest *        req;

 iGopher              gopher;

 iGmResponse *        resp;

 iBool                isRespLocked;

 iBool                isRespFiltered;

 iAtomicInt           allowUpdate;

@@ -208,7 +209,7 @@ static int processIncomingData_GmRequest_(iGmRequest *d, const iBlock *data) {

             resp->statusCode = code;

             d->state         = receivingBody_GmRequestState;

             notifyUpdate     = iTrue;

                 d->isRespFiltered = iTrue;

             }

         }

@@ -226,7 +227,12 @@ static int processIncomingData_GmRequest_(iGmRequest *d, const iBlock *data) {

static void readIncoming_GmRequest_(iGmRequest *d, iTlsRequest *req) {

 lock_Mutex(d->mtx);

 iGmResponse *resp = d->resp;

 iBlock *  data         = readAll_TlsRequest(req);

 const int ubits        = processIncomingData_GmRequest_(d, data);

 iBool     notifyUpdate = (ubits & 1) != 0;

@@ -457,8 +463,9 @@ static void beginGopherConnection_GmRequest_(iGmRequest *d, const iString *host,

void init_GmRequest(iGmRequest *d, iGmCerts *certs) {

 d->mtx = new_Mutex();

 d->resp = new_GmResponse();

 set_Atomic(&d->allowUpdate, iTrue);

 init_String(&d->url);

 init_Gopher(&d->gopher);

@@ -492,6 +499,10 @@ void deinit_GmRequest(iGmRequest *d) {

 delete_Mutex(d->mtx);

}

+void enableFilters_GmRequest(iGmRequest *d, iBool enable) {

+}

void setUrl_GmRequest(iGmRequest *d, const iString *url) {

 set_String(&d->url, urlFragmentStripped_String(url));

 /* Encode hostname to Punycode here because we want to submit the Punycode domain name

@@ -546,7 +557,7 @@ void submit_GmRequest(iGmRequest *d) {

         remove_Block(&path->chars, 0, 1);

     }

#endif

     if (open_File(f, readOnly_FileMode)) {

         /* TODO: Check supported file types: images, audio */

         /* TODO: Detect text files based on contents? E.g., is the content valid UTF-8. */

diff --git a/src/gmrequest.h b/src/gmrequest.h

index bd340cf1..6d4eb2f8 100644

--- a/src/gmrequest.h

+++ b/src/gmrequest.h

@@ -64,6 +64,7 @@ iDeclareNotifyFunc(GmRequest, Finished)

iDeclareAudienceGetter(GmRequest, updated)

iDeclareAudienceGetter(GmRequest, finished)

+void enableFilters_GmRequest (iGmRequest *, iBool enable);

void setUrl_GmRequest (iGmRequest *, const iString *url);

void submit_GmRequest (iGmRequest *);

void cancel_GmRequest (iGmRequest *);

diff --git a/src/gmutil.c b/src/gmutil.c

index 44bbabfd..2b40367d 100644

--- a/src/gmutil.c

+++ b/src/gmutil.c

@@ -76,6 +76,9 @@ iLocalDef iBool isDef_(iRangecc cc) {

static iRangecc prevPathSeg_(const char *end, const char *start) {

 iRangecc seg = { end, end };

 do {

     seg.start--;

 } while (*seg.start != '/' && seg.start != start);

@@ -149,6 +152,37 @@ iRangecc urlHost_String(const iString *d) {

 return url.host;

}

+iRangecc urlUser_String(const iString *d) {

+}

+iRangecc urlRoot_String(const iString *d) {

+}

static iBool isAbsolutePath_(iRangecc path) {

 return isAbsolute_Path(collect_String(urlDecode_String(collect_String(newRange_String(path)))));

}

@@ -197,7 +231,7 @@ void urlEncodePath_String(iString *d) {

     return;

 }

 iString *encoded = new_String();

 iString *path    = newRange_String(url.path);

 iString *encPath = urlEncodeExclude_String(path, "%/ ");

 append_String(encoded, encPath);

@@ -272,6 +306,21 @@ const iString *absoluteUrl_String(const iString *d, const iString *urlMaybeRelat

 return absolute;

}

+iBool isLikelyUrl_String(const iString *d) {

+}

static iBool equalPuny_(const iString *d, iRangecc orig) {

 if (!endsWith_String(d, "-")) {

     return iFalse; /* This is a sufficient condition? */

diff --git a/src/gmutil.h b/src/gmutil.h

index 1caf2445..b2cee61a 100644

--- a/src/gmutil.h

+++ b/src/gmutil.h

@@ -103,7 +103,10 @@ void init_Url (iUrl *, const iString *text);

iRangecc urlScheme_String (const iString *);

iRangecc urlHost_String (const iString *);

+iRangecc urlUser_String (const iString *);

+iRangecc urlRoot_String (const iString *);

const iString * absoluteUrl_String (const iString *, const iString *urlMaybeRelative);

+iBool isLikelyUrl_String (const iString *);

void punyEncodeUrlHost_String(iString *);

void stripDefaultUrlPort_String(iString *);

const iString * urlFragmentStripped_String(const iString *);

diff --git a/src/gopher.c b/src/gopher.c

index 0a7489ba..fa8495d7 100644

--- a/src/gopher.c

+++ b/src/gopher.c

@@ -160,11 +160,11 @@ void open_Gopher(iGopher *d, const iString *url) {

     d->type = '0';

 }

 else if (parts.path.start < parts.path.end) {

 }

 else {

 }

 if (d->type == '7' && isEmpty_Range(&parts.query)) {

     /* Ask for the query parameters first. */

@@ -204,12 +204,16 @@ void open_Gopher(iGopher *d, const iString *url) {

 }

 d->isPre = iFalse;

 open_Socket(d->socket);

 if (!isEmpty_Range(&parts.query)) {

     iAssert(*parts.query.start == '?');

     parts.query.start++;

     writeData_Socket(d->socket, "\t", 1);

 }

 writeData_Socket(d->socket, "\r\n", 2);

}

diff --git a/src/history.c b/src/history.c

index 59d515dc..6876d8e3 100644

--- a/src/history.c

+++ b/src/history.c

@@ -299,6 +299,18 @@ size_t cacheSize_History(const iHistory *d) {

 return cached;

}

+void clearCache_History(iHistory *d) {

+}

size_t pruneLeastImportant_History(iHistory *d) {

 size_t delta  = 0;

 size_t chosen = iInvalidPos;

diff --git a/src/history.h b/src/history.h

index 7c2684f1..ce3b8e47 100644

--- a/src/history.h

+++ b/src/history.h

@@ -56,6 +56,7 @@ iBool goForward_History (iHistory *);

iRecentUrl *recentUrl_History (iHistory *, size_t pos);

iRecentUrl *mostRecentUrl_History (iHistory *);

iRecentUrl *findUrl_History (iHistory *, const iString *url);

+void clearCache_History (iHistory *);

size_t pruneLeastImportant_History (iHistory *);

const iStringArray * searchContents_History (const iHistory *, const iRegExp pattern); / chronologically ascending */

diff --git a/src/ios.h b/src/ios.h

new file mode 100644

index 00000000..60841aee

--- /dev/null

+++ b/src/ios.h

@@ -0,0 +1,33 @@

+/* Copyright 2021 Jaakko Keränen jaakko.keranen@iki.fi

+Redistribution and use in source and binary forms, with or without

+modification, are permitted provided that the following conditions are met:

+1. Redistributions of source code must retain the above copyright notice, this

+2. Redistributions in binary form must reproduce the above copyright notice,

+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND

+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED

+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE

+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR

+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES

+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;

+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON

+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT

+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS

+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */

+#pragma once

+#include "ui/util.h"

+iDeclareType(Window)

+void setupApplication_iOS (void);

+void setupWindow_iOS (iWindow *window);

+iBool isPhone_iOS (void);

+void safeAreaInsets_iOS (float *left, float *top, float *right, float *bottom);

+iBool processEvent_iOS (const SDL_Event *);

diff --git a/src/ios.m b/src/ios.m

new file mode 100644

index 00000000..5abf87df

--- /dev/null

+++ b/src/ios.m

@@ -0,0 +1,105 @@

+/* Copyright 2021 Jaakko Keränen jaakko.keranen@iki.fi

+Redistribution and use in source and binary forms, with or without

+modification, are permitted provided that the following conditions are met:

+1. Redistributions of source code must retain the above copyright notice, this

+2. Redistributions in binary form must reproduce the above copyright notice,

+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND

+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED

+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE

+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR

+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES

+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;

+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON

+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT

+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS

+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */

+#include "ios.h"

+#include "app.h"

+#include "ui/window.h"

+#include <SDL_events.h>

+#include <SDL_syswm.h>

+#import <UIKit/UIKit.h>

+static iBool isSystemDarkMode_ = iFalse;

+static iBool isPhone_ = iFalse;

+static void enableMouse_(iBool yes) {

+}

+void setupApplication_iOS(void) {

+}

+static UIViewController *viewController_(iWindow *window) {

+}

+static iBool isDarkMode_(iWindow *window) {

+}

+void safeAreaInsets_iOS(float *left, float *top, float *right, float *bottom) {

+}

+iBool isPhone_iOS(void) {

+}

+void setupWindow_iOS(iWindow *window) {

+}

+iBool processEvent_iOS(const SDL_Event *ev) {

+}

diff --git a/src/main.c b/src/main.c

index 5e4e7284..21c648ba 100644

--- a/src/main.c

+++ b/src/main.c

@@ -22,9 +22,12 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */

#include "app.h"

-#if defined (iPlatformApple)

+#if defined (iPlatformAppleDesktop)

include "macos.h"

#endif

+#if defined (iPlatformAppleMobile)

+# include "ios.h"

+#endif

#if defined (iPlatformMsys)

include "win32.h"

define SDL_MAIN_HANDLED

@@ -34,6 +37,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */

#endif

#include <the_Foundation/commandline.h>

+#include <the_Foundation/tlsrequest.h>

#include <SDL.h>

#include <stdio.h>

#include <signal.h>

@@ -41,7 +45,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */

int main(int argc, char **argv) {

 printf("Lagrange: A Beautiful Gemini Client\n");

 signal(SIGPIPE, SIG_IGN);

-#if defined (iPlatformApple)

+#if defined (iPlatformAppleDesktop)

 enableMomentumScroll_MacOS();

 registerURLHandler_MacOS();

#endif

@@ -55,6 +59,13 @@ int main(int argc, char **argv) {

 mpg123_init();

#endif

 init_Foundation();

 SDL_SetHint(SDL_HINT_VIDEO_ALLOW_SCREENSAVER, "1");

 SDL_SetHint(SDL_HINT_MAC_CTRL_CLICK_EMULATE_RIGHT_CLICK, "1");

 if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)) {

diff --git a/src/media.c b/src/media.c

index 8bd635a5..b329ec03 100644

--- a/src/media.c

+++ b/src/media.c

@@ -27,26 +27,31 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */

#include "audio/player.h"

#include "app.h"

+#include <the_Foundation/file.h>

#include <the_Foundation/ptrarray.h>

#include <stb_image.h>

#include <SDL_hints.h>

#include <SDL_render.h>

+#include <SDL_timer.h>

iDeclareType(GmMediaProps)

struct Impl_GmMediaProps {

 iGmLinkId linkId;

 iString   mime;

 iBool     isPermanent;

};

static void init_GmMediaProps_(iGmMediaProps *d) {

 d->linkId = 0;

 init_String(&d->mime);

 d->isPermanent = iFalse;

}

static void deinit_GmMediaProps_(iGmMediaProps *d) {

 deinit_String(&d->mime);

}

@@ -125,9 +130,77 @@ iDefineTypeConstruction(GmAudio)

/----------------------------------------------------------------------------------------------/

+iDeclareType(GmDownload)

+struct Impl_GmDownload {

+};

+static iBool openFile_GmDownload_(iGmDownload *d) {

+}

+static void closeFile_GmDownload_(iGmDownload *d) {

+}

+void init_GmDownload(iGmDownload *d) {

+}

+void deinit_GmDownload(iGmDownload *d) {

+}

+static void writeToFile_GmDownload_(iGmDownload *d, const iBlock *data) {

+}

+iDefineTypeConstruction(GmDownload)

+/----------------------------------------------------------------------------------------------/

struct Impl_Media {

 iPtrArray images;

};

iDefineTypeConstruction(Media)

@@ -135,10 +208,12 @@ iDefineTypeConstruction(Media)

void init_Media(iMedia *d) {

 init_PtrArray(&d->images);

 init_PtrArray(&d->audio);

}

void deinit_Media(iMedia *d) {

 clear_Media(d);

 deinit_PtrArray(&d->audio);

 deinit_PtrArray(&d->images);

}

@@ -152,6 +227,29 @@ void clear_Media(iMedia *d) {

     deinit_GmAudio(a.ptr);

 }

 clear_PtrArray(&d->audio);

+}

+iBool setDownloadUrl_Media(iMedia *d, iGmLinkId linkId, const iString *url) {

}

iBool setData_Media(iMedia *d, iGmLinkId linkId, const iString *mime, const iBlock *data,

@@ -195,6 +293,26 @@ iBool setData_Media(iMedia *d, iGmLinkId linkId, const iString *mime, const iBlo

         }

     }

 }

 else if (!isDeleting) {

     if (startsWith_String(mime, "image/")) {

         /* Copy the image to a texture. */

@@ -253,6 +371,24 @@ iMediaId findLinkAudio_Media(const iMedia *d, iGmLinkId linkId) {

 return 0;

}

+iMediaId findLinkDownload_Media(const iMedia *d, uint16_t linkId) {

+}

+iInt2 imageSize_Media(const iMedia *d, iMediaId imageId) {

+}

SDL_Texture *imageTexture_Media(const iMedia *d, uint16_t imageId) {

 if (imageId > 0 && imageId <= size_PtrArray(&d->images)) {

     const iGmImage *img = constAt_PtrArray(&d->images, imageId - 1);

@@ -261,12 +397,11 @@ SDL_Texture *imageTexture_Media(const iMedia *d, uint16_t imageId) {

 return NULL;

}

-iBool imageInfo_Media(const iMedia *d, iMediaId imageId, iGmImageInfo *info_out) {

+iBool imageInfo_Media(const iMedia *d, iMediaId imageId, iGmMediaInfo *info_out) {

 if (imageId > 0 && imageId <= size_PtrArray(&d->images)) {

     const iGmImage *img   = constAt_PtrArray(&d->images, imageId - 1);

     info_out->numBytes    = img->numBytes;

     info_out->isPermanent = img->props.isPermanent;

     return iTrue;

 }

@@ -282,10 +417,10 @@ iPlayer *audioData_Media(const iMedia *d, iMediaId audioId) {

 return NULL;

}

-iBool audioInfo_Media(const iMedia *d, iMediaId audioId, iGmAudioInfo *info_out) {

+iBool audioInfo_Media(const iMedia *d, iMediaId audioId, iGmMediaInfo *info_out) {

 if (audioId > 0 && audioId <= size_PtrArray(&d->audio)) {

     const iGmAudio *audio = constAt_PtrArray(&d->audio, audioId - 1);

     info_out->isPermanent = audio->props.isPermanent;

     return iTrue;

 }

@@ -301,6 +436,33 @@ iPlayer *audioPlayer_Media(const iMedia *d, iMediaId audioId) {

 return NULL;

}

+iBool downloadInfo_Media(const iMedia *d, iMediaId downloadId, iGmMediaInfo *info_out) {

+}

+void downloadStats_Media(const iMedia *d, iMediaId downloadId, const iString **path_out,

+}

/----------------------------------------------------------------------------------------------/

static void updated_MediaRequest_(iAnyObject *obj) {

@@ -313,11 +475,13 @@ static void finished_MediaRequest_(iAnyObject *obj) {

 postCommandf_App("media.finished link:%u request:%p", d->linkId, d);

}

-void init_MediaRequest(iMediaRequest *d, iDocumentWidget *doc, unsigned int linkId, const iString *url) {

+void init_MediaRequest(iMediaRequest *d, iDocumentWidget *doc, unsigned int linkId,

 d->doc    = doc;

 d->linkId = linkId;

 d->req    = new_GmRequest(certs_App());

 setUrl_GmRequest(d->req, url);

 iConnect(GmRequest, d->req, updated, d, updated_MediaRequest_);

 iConnect(GmRequest, d->req, finished, d, finished_MediaRequest_);

 submit_GmRequest(d->req);

@@ -330,6 +494,7 @@ void deinit_MediaRequest(iMediaRequest *d) {

}

iDefineObjectConstructionArgs(MediaRequest,

iDefineClass(MediaRequest)

diff --git a/src/media.h b/src/media.h

index ddaa2d3c..7cc941d0 100644

--- a/src/media.h

+++ b/src/media.h

@@ -30,18 +30,11 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */

typedef uint16_t iMediaId;

iDeclareType(Player)

-iDeclareType(GmImageInfo)

-iDeclareType(GmAudioInfo)

+iDeclareType(GmMediaInfo)

-struct Impl_GmImageInfo {

+struct Impl_GmMediaInfo {

 size_t      numBytes;

-};

-struct Impl_GmAudioInfo {

 iBool       isPermanent;

};

@@ -53,18 +46,24 @@ enum iMediaFlags {

 partialData_MediaFlag = iBit(2),

};

-void clear_Media (iMedia *);

-iBool setData_Media (iMedia *, uint16_t linkId, const iString *mime, const iBlock *data, int flags);

+void clear_Media (iMedia *);

+iBool setDownloadUrl_Media (iMedia *, uint16_t linkId, const iString *url);

+iBool setData_Media (iMedia *, uint16_t linkId, const iString *mime, const iBlock *data, int flags);

iMediaId findLinkImage_Media (const iMedia *, uint16_t linkId);

-iBool imageInfo_Media (const iMedia *, iMediaId imageId, iGmImageInfo *info_out);

+iBool imageInfo_Media (const iMedia *, iMediaId imageId, iGmMediaInfo *info_out);

+iInt2 imageSize_Media (const iMedia *, iMediaId imageId);

SDL_Texture * imageTexture_Media (const iMedia *, iMediaId imageId);

size_t numAudio_Media (const iMedia *);

iMediaId findLinkAudio_Media (const iMedia *, uint16_t linkId);

-iBool audioInfo_Media (const iMedia *, iMediaId audioId, iGmAudioInfo *info_out);

+iBool audioInfo_Media (const iMedia *, iMediaId audioId, iGmMediaInfo *info_out);

iPlayer * audioPlayer_Media (const iMedia *, iMediaId audioId);

+iMediaId findLinkDownload_Media (const iMedia *, uint16_t linkId);

+iBool downloadInfo_Media (const iMedia *, iMediaId downloadId, iGmMediaInfo *info_out);

+void downloadStats_Media (const iMedia *, iMediaId downloadId, const iString **path_out,

/----------------------------------------------------------------------------------------------/

@@ -81,4 +80,4 @@ struct Impl_MediaRequest {

};

iDeclareObjectConstructionArgs(MediaRequest, iDocumentWidget *doc, unsigned int linkId,

diff --git a/src/mimehooks.c b/src/mimehooks.c

index f4ec6bf4..3b3c0d1a 100644

--- a/src/mimehooks.c

+++ b/src/mimehooks.c

@@ -6,6 +6,7 @@

#include <the_Foundation/path.h>

#include <the_Foundation/process.h>

#include <the_Foundation/stringlist.h>

+#include <the_Foundation/xml.h>

iDefineTypeConstruction(FilterHook)

@@ -69,6 +70,115 @@ iBlock *run_FilterHook_(const iFilterHook *d, const iString *mime, const iBlock

/----------------------------------------------------------------------------------------------/

+static iRegExp *xmlMimePattern_(void) {

+}

+static iBlock *translateAtomXmlToGeminiFeed_(const iString *mime, const iBlock *source,

+finished:

+}

+/----------------------------------------------------------------------------------------------/

struct Impl_MimeHooks {

 iPtrArray filters;

};

@@ -87,7 +197,7 @@ void deinit_MimeHooks(iMimeHooks *d) {

}

iBool willTryFilter_MimeHooks(const iMimeHooks *d, const iString *mime) {

 iRegExpMatch m;

 iConstForEach(PtrArray, i, &d->filters) {

     const iFilterHook *xc = i.ptr;

@@ -96,6 +206,11 @@ iBool willTryFilter_MimeHooks(const iMimeHooks *d, const iString *mime) {

         return iTrue;

     }

 }

 return iFalse;

}

@@ -112,6 +227,14 @@ iBlock *tryFilter_MimeHooks(const iMimeHooks *d, const iString *mime, const iBlo

         }

     }

 }

 return NULL;

}

diff --git a/src/prefs.c b/src/prefs.c

index fe755c63..97ad7f48 100644

--- a/src/prefs.c

+++ b/src/prefs.c

@@ -26,6 +26,8 @@ void init_Prefs(iPrefs *d) {

 d->dialogTab         = 0;

 d->useSystemTheme    = iTrue;

 d->theme             = dark_ColorTheme;

 d->retainWindowSize  = iTrue;

 d->uiScale           = 1.0f; /* default set elsewhere */

 d->zoomPercent       = 100;

@@ -42,6 +44,7 @@ void init_Prefs(iPrefs *d) {

 d->lineWidth         = 38;

 d->bigFirstParagraph = iTrue;

 d->quoteIcon         = iTrue;

 d->docThemeDark      = colorfulDark_GmDocumentTheme;

 d->docThemeLight     = white_GmDocumentTheme;

 d->saturation        = 1.0f;

@@ -49,9 +52,14 @@ void init_Prefs(iPrefs *d) {

 init_String(&d->gopherProxy);

 init_String(&d->httpProxy);

 init_String(&d->downloadDir);

+#if defined (iPlatformAppleMobile)

+#endif

}

void deinit_Prefs(iPrefs *d) {

 deinit_String(&d->geminiProxy);

 deinit_String(&d->gopherProxy);

 deinit_String(&d->httpProxy);

diff --git a/src/prefs.h b/src/prefs.h

index 1c3274d9..4bbe3ad5 100644

--- a/src/prefs.h

+++ b/src/prefs.h

@@ -38,6 +38,8 @@ struct Impl_Prefs {

 /* Window */

 iBool            useSystemTheme;

 enum iColorTheme theme;

 iBool            retainWindowSize;

 float            uiScale;

 int              zoomPercent;

@@ -47,6 +49,7 @@ struct Impl_Prefs {

 iBool            hoverLink;

 iBool            smoothScrolling;

 iBool            loadImageInsteadOfScrolling;

 /* Network */

 iBool            decodeUserVisibleURLs;

 int              maxCacheSize; /* MB */

@@ -61,6 +64,7 @@ struct Impl_Prefs {

 int              lineWidth;

 iBool            bigFirstParagraph;

 iBool            quoteIcon;

 /* Colors */

 enum iGmDocumentTheme docThemeDark;

 enum iGmDocumentTheme docThemeLight;

diff --git a/src/stb_image_resize.h b/src/stb_image_resize.h

new file mode 100644

index 00000000..42a8efb1

--- /dev/null

+++ b/src/stb_image_resize.h

@@ -0,0 +1,2631 @@

+/* stb_image_resize - v0.96 - public domain image resizing

+*/

+#ifndef STBIR_INCLUDE_STB_IMAGE_RESIZE_H

+#define STBIR_INCLUDE_STB_IMAGE_RESIZE_H

+#ifdef _MSC_VER

+typedef unsigned char stbir_uint8;

+typedef unsigned short stbir_uint16;

+typedef unsigned int stbir_uint32;

+#else

+#include <stdint.h>

+typedef uint8_t stbir_uint8;

+typedef uint16_t stbir_uint16;

+typedef uint32_t stbir_uint32;

+#endif

+#ifndef STBIRDEF

+#ifdef STB_IMAGE_RESIZE_STATIC

+#define STBIRDEF static

+#else

+#ifdef __cplusplus

+#define STBIRDEF extern "C"

+#else

+#define STBIRDEF extern

+#endif

+#endif

+#endif

+//////////////////////////////////////////////////////////////////////////////

+//

+// Easy-to-use API:

+//

+// * "input pixels" points to an array of image data with 'num_channels' channels (e.g. RGB=3, RGBA=4)

+// * input_w is input image width (x-axis), input_h is input image height (y-axis)

+// * stride is the offset between successive rows of image data in memory, in bytes. you can

+// specify 0 to mean packed continuously in memory

+// * alpha channel is treated identically to other channels.

+// * colorspace is linear or sRGB as specified by function name

+// * returned result is 1 for success or 0 in case of an error.

+// #define STBIR_ASSERT() to trigger an assert on parameter validation errors.

+// * Memory required grows approximately linearly with input and output size, but with

+// discontinuities at input_w == output_w and input_h == output_h.

+// * These functions use a "default" resampling filter defined at compile time. To change the filter,

+// you can change the compile-time defaults by #defining STBIR_DEFAULT_FILTER_UPSAMPLE

+// and STBIR_DEFAULT_FILTER_DOWNSAMPLE, or you can use the medium-complexity API.

+STBIRDEF int stbir_resize_uint8( const unsigned char *input_pixels , int input_w , int input_h , int input_stride_in_bytes,

+STBIRDEF int stbir_resize_float( const float *input_pixels , int input_w , int input_h , int input_stride_in_bytes,

+// The following functions interpret image data as gamma-corrected sRGB.

+// Specify STBIR_ALPHA_CHANNEL_NONE if you have no alpha channel,

+// or otherwise provide the index of the alpha channel. Flags value

+// of 0 will probably do the right thing if you're not sure what

+// the flags mean.

+#define STBIR_ALPHA_CHANNEL_NONE -1

+// Set this flag if your texture has premultiplied alpha. Otherwise, stbir will

+// use alpha-weighted resampling (effectively premultiplying, resampling,

+// then unpremultiplying).

+#define STBIR_FLAG_ALPHA_PREMULTIPLIED (1 << 0)

+// The specified alpha channel should be handled as gamma-corrected value even

+// when doing sRGB operations.

+#define STBIR_FLAG_ALPHA_USES_COLORSPACE (1 << 1)

+STBIRDEF int stbir_resize_uint8_srgb(const unsigned char *input_pixels , int input_w , int input_h , int input_stride_in_bytes,

+typedef enum

+{

+} stbir_edge;

+// This function adds the ability to specify how requests to sample off the edge of the image are handled.

+STBIRDEF int stbir_resize_uint8_srgb_edgemode(const unsigned char *input_pixels , int input_w , int input_h , int input_stride_in_bytes,

+//////////////////////////////////////////////////////////////////////////////

+//

+// Medium-complexity API

+//

+// This extends the easy-to-use API as follows:

+//

+// * Alpha-channel can be processed separately

+// * If alpha_channel is not STBIR_ALPHA_CHANNEL_NONE

+// * Alpha channel will not be gamma corrected (unless flags&STBIR_FLAG_GAMMA_CORRECT)

+// * Filters will be weighted by alpha channel (unless flags&STBIR_FLAG_ALPHA_PREMULTIPLIED)

+// * Filter can be selected explicitly

+// * uint16 image type

+// * sRGB colorspace available for all types

+// * context parameter for passing to STBIR_MALLOC

+typedef enum

+{

+} stbir_filter;

+typedef enum

+{

+} stbir_colorspace;

+// The following functions are all identical except for the type of the image data

+STBIRDEF int stbir_resize_uint8_generic( const unsigned char *input_pixels , int input_w , int input_h , int input_stride_in_bytes,

+STBIRDEF int stbir_resize_uint16_generic(const stbir_uint16 *input_pixels , int input_w , int input_h , int input_stride_in_bytes,

+STBIRDEF int stbir_resize_float_generic( const float *input_pixels , int input_w , int input_h , int input_stride_in_bytes,

+//////////////////////////////////////////////////////////////////////////////

+//

+// Full-complexity API

+//

+// This extends the medium API as follows:

+//

+// * uint32 image type

+// * not typesafe

+// * separate filter types for each axis

+// * separate edge modes for each axis

+// * can specify scale explicitly for subpixel correctness

+// * can specify image source tile using texture coordinates

+typedef enum

+{

+} stbir_datatype;

+STBIRDEF int stbir_resize( const void *input_pixels , int input_w , int input_h , int input_stride_in_bytes,

+STBIRDEF int stbir_resize_subpixel(const void *input_pixels , int input_w , int input_h , int input_stride_in_bytes,

+STBIRDEF int stbir_resize_region( const void *input_pixels , int input_w , int input_h , int input_stride_in_bytes,

+// (s0, t0) & (s1, t1) are the top-left and bottom right corner (uv addressing style: [0, 1]x[0, 1]) of a region of the input image to use.

+//

+//

+//// end header file /////////////////////////////////////////////////////

+#endif // STBIR_INCLUDE_STB_IMAGE_RESIZE_H

+#ifdef STB_IMAGE_RESIZE_IMPLEMENTATION

+#ifndef STBIR_ASSERT

+#include <assert.h>

+#define STBIR_ASSERT(x) assert(x)

+#endif

+// For memset

+#include <string.h>

+#include <math.h>

+#ifndef STBIR_MALLOC

+#include <stdlib.h>

+// use comma operator to evaluate c, to avoid "unused parameter" warnings

+#define STBIR_MALLOC(size,c) ((void)(c), malloc(size))

+#define STBIR_FREE(ptr,c) ((void)(c), free(ptr))

+#endif

+#ifndef _MSC_VER

+#ifdef __cplusplus

+#define stbir__inline inline

+#else

+#define stbir__inline

+#endif

+#else

+#define stbir__inline __forceinline

+#endif

+// should produce compiler error if size is wrong

+typedef unsigned char stbir__validate_uint32[sizeof(stbir_uint32) == 4 ? 1 : -1];

+#ifdef _MSC_VER

+#define STBIR__NOTUSED(v) (void)(v)

+#else

+#define STBIR__NOTUSED(v) (void)sizeof(v)

+#endif

+#define STBIR__ARRAY_SIZE(a) (sizeof((a))/sizeof((a)[0]))

+#ifndef STBIR_DEFAULT_FILTER_UPSAMPLE

+#define STBIR_DEFAULT_FILTER_UPSAMPLE STBIR_FILTER_CATMULLROM

+#endif

+#ifndef STBIR_DEFAULT_FILTER_DOWNSAMPLE

+#define STBIR_DEFAULT_FILTER_DOWNSAMPLE STBIR_FILTER_MITCHELL

+#endif

+#ifndef STBIR_PROGRESS_REPORT

+#define STBIR_PROGRESS_REPORT(float_0_to_1)

+#endif

+#ifndef STBIR_MAX_CHANNELS

+#define STBIR_MAX_CHANNELS 64

+#endif

+#if STBIR_MAX_CHANNELS > 65536

+#error "Too many channels; STBIR_MAX_CHANNELS must be no more than 65536."

+// because we store the indices in 16-bit variables

+#endif

+// This value is added to alpha just before premultiplication to avoid

+// zeroing out color values. It is equivalent to 2^-80. If you don't want

+// that behavior (it may interfere if you have floating point images with

+// very small alpha values) then you can define STBIR_NO_ALPHA_EPSILON to

+// disable it.

+#ifndef STBIR_ALPHA_EPSILON

+#define STBIR_ALPHA_EPSILON ((float)1 / (1 << 20) / (1 << 20) / (1 << 20) / (1 << 20))

+#endif

+#ifdef _MSC_VER

+#define STBIR__UNUSED_PARAM(v) (void)(v)

+#else

+#define STBIR__UNUSED_PARAM(v) (void)sizeof(v)

+#endif

+// must match stbir_datatype

+static unsigned char stbir__type_size[] = {

+};

+// Kernel function centered at 0

+typedef float (stbir__kernel_fn)(float x, float scale);

+typedef float (stbir__support_fn)(float scale);

+typedef struct

+{

+} stbir__filter_info;

+// When upsampling, the contributors are which source pixels contribute.

+// When downsampling, the contributors are which destination pixels are contributed to.

+typedef struct

+{

+} stbir__contributors;

+typedef struct

+{

+} stbir__info;

+static const float stbir__max_uint8_as_float = 255.0f;

+static const float stbir__max_uint16_as_float = 65535.0f;

+static const double stbir__max_uint32_as_float = 4294967295.0;

+static stbir__inline int stbir__min(int a, int b)

+{

+}

+static stbir__inline float stbir__saturate(float x)

+{

+}

+#ifdef STBIR_SATURATE_INT

+static stbir__inline stbir_uint8 stbir__saturate8(int x)

+{

+}

+static stbir__inline stbir_uint16 stbir__saturate16(int x)

+{

+}

+#endif

+static float stbir__srgb_uchar_to_linear_float[256] = {

+};

+static float stbir__srgb_to_linear(float f)

+{

+}

+static float stbir__linear_to_srgb(float f)

+{

+}

+#ifndef STBIR_NON_IEEE_FLOAT

+// From https://gist.github.com/rygorous/2203834

+typedef union

+{

+} stbir__FP32;

+static const stbir_uint32 fp32_to_srgb8_tab4[104] = {

+};

+static stbir_uint8 stbir__linear_to_srgb_uchar(float in)

+{

+}

+#else

+// sRGB transition values, scaled by 1<<28

+static int stbir__srgb_offset_to_linear_scaled[256] =

+{

+};

+static stbir_uint8 stbir__linear_to_srgb_uchar(float f)

+{

+}

+#endif

+static float stbir__filter_trapezoid(float x, float scale)

+{

+}

+static float stbir__support_trapezoid(float scale)

+{

+}

+static float stbir__filter_triangle(float x, float s)

+{

+}

+static float stbir__filter_cubic(float x, float s)

+{

+}

+static float stbir__filter_catmullrom(float x, float s)

+{

+}

+static float stbir__filter_mitchell(float x, float s)

+{

+}

+static float stbir__support_zero(float s)

+{

+}

+static float stbir__support_one(float s)

+{

+}

+static float stbir__support_two(float s)

+{

+}

+static stbir__filter_info stbir__filter_info_table[] = {

+};

+stbir__inline static int stbir__use_upsampling(float ratio)

+{

+}

+stbir__inline static int stbir__use_width_upsampling(stbir__info* stbir_info)

+{

+}

+stbir__inline static int stbir__use_height_upsampling(stbir__info* stbir_info)

+{

+}

+// This is the maximum number of input samples that can affect an output sample

+// with the given filter

+static int stbir__get_filter_pixel_width(stbir_filter filter, float scale)

+{

+}

+// This is how much to expand buffers to account for filters seeking outside

+// the image boundaries.

+static int stbir__get_filter_pixel_margin(stbir_filter filter, float scale)

+{

+}

+static int stbir__get_coefficient_width(stbir_filter filter, float scale)

+{

+}

+static int stbir__get_contributors(float scale, stbir_filter filter, int input_size, int output_size)

+{

+}

+static int stbir__get_total_horizontal_coefficients(stbir__info* info)

+{

+}

+static int stbir__get_total_vertical_coefficients(stbir__info* info)

+{

+}

+static stbir__contributors* stbir__get_contributor(stbir__contributors* contributors, int n)

+{

+}

+// For perf reasons this code is duplicated in stbir__resample_horizontal_upsample/downsample,

+// if you change it here change it there too.

+static float* stbir__get_coefficient(float* coefficients, stbir_filter filter, float scale, int n, int c)

+{

+}

+static int stbir__edge_wrap_slow(stbir_edge edge, int n, int max)

+{

+}

+stbir__inline static int stbir__edge_wrap(stbir_edge edge, int n, int max)

+{

+}

+// What input pixels contribute to this output pixel?

+static void stbir__calculate_sample_range_upsample(int n, float out_filter_radius, float scale_ratio, float out_shift, int* in_first_pixel, int* in_last_pixel, float* in_center_of_out)

+{

+}

+// What output pixels does this input pixel contribute to?

+static void stbir__calculate_sample_range_downsample(int n, float in_pixels_radius, float scale_ratio, float out_shift, int* out_first_pixel, int* out_last_pixel, float* out_center_of_in)

+{

+}

+static void stbir__calculate_coefficients_upsample(stbir_filter filter, float scale, int in_first_pixel, int in_last_pixel, float in_center_of_out, stbir__contributors* contributor, float* coefficient_group)

+{

+}

+static void stbir__calculate_coefficients_downsample(stbir_filter filter, float scale_ratio, int out_first_pixel, int out_last_pixel, float out_center_of_in, stbir__contributors* contributor, float* coefficient_group)

+{

+}

+static void stbir__normalize_downsample_coefficients(stbir__contributors* contributors, float* coefficients, stbir_filter filter, float scale_ratio, int input_size, int output_size)

+{

+}

+// Each scan line uses the same kernel values so we should calculate the kernel

+// values once and then we can use them for every scan line.

+static void stbir__calculate_filters(stbir__contributors* contributors, float* coefficients, stbir_filter filter, float scale_ratio, float shift, int input_size, int output_size)

+{

+}

+static float* stbir__get_decode_buffer(stbir__info* stbir_info)

+{

+}

+#define STBIR__DECODE(type, colorspace) ((int)(type) * (STBIR_MAX_COLORSPACES) + (int)(colorspace))

+static void stbir__decode_scanline(stbir__info* stbir_info, int n)

+{

+#ifndef STBIR_NO_ALPHA_EPSILON

+#endif

+}

+static float* stbir__get_ring_buffer_entry(float* ring_buffer, int index, int ring_buffer_length)

+{

+}

+static float* stbir__add_empty_ring_buffer_entry(stbir__info* stbir_info, int n)

+{

+}

+static void stbir__resample_horizontal_upsample(stbir__info* stbir_info, float* output_buffer)

+{

+}

+static void stbir__resample_horizontal_downsample(stbir__info* stbir_info, float* output_buffer)

+{

+}

+static void stbir__decode_and_resample_upsample(stbir__info* stbir_info, int n)

+{

+}

+static void stbir__decode_and_resample_downsample(stbir__info* stbir_info, int n)

+{

+}

+// Get the specified scan line from the ring buffer.

+static float* stbir__get_ring_buffer_scanline(int get_scanline, float* ring_buffer, int begin_index, int first_scanline, int ring_buffer_num_entries, int ring_buffer_length)

+{

+}

+static void stbir__encode_scanline(stbir__info* stbir_info, int num_pixels, void *output_buffer, float *encode_buffer, int channels, int alpha_channel, int decode)

+{

+}

+static void stbir__resample_vertical_upsample(stbir__info* stbir_info, int n)

+{

+}

+static void stbir__resample_vertical_downsample(stbir__info* stbir_info, int n)

+{

+}

+static void stbir__buffer_loop_upsample(stbir__info* stbir_info)

+{

+}

+static void stbir__empty_ring_buffer(stbir__info* stbir_info, int first_necessary_scanline)

+{

+}

+static void stbir__buffer_loop_downsample(stbir__info* stbir_info)

+{

+}

+static void stbir__setup(stbir__info *info, int input_w, int input_h, int output_w, int output_h, int channels)

+{

+}

+static void stbir__calculate_transform(stbir__info *info, float s0, float t0, float s1, float t1, float *transform)

+{

+}

+static void stbir__choose_filter(stbir__info *info, stbir_filter h_filter, stbir_filter v_filter)

+{

+}

+static stbir_uint32 stbir__calculate_memory(stbir__info *info)

+{

+}

+static int stbir__resize_allocated(stbir__info *info,

+{

+#ifdef STBIR_DEBUG_OVERWRITE_TEST

+#define OVERWRITE_ARRAY_SIZE 8

+#endif

+#define STBIR__NEXT_MEMPTR(current, newtype) (newtype*)(((unsigned char*)current) + current##_size)

+#undef STBIR__NEXT_MEMPTR

+#ifdef STBIR_DEBUG_OVERWRITE_TEST

+#endif

+}

+static int stbir__resize_arbitrary(

+{

+}

+STBIRDEF int stbir_resize_uint8( const unsigned char *input_pixels , int input_w , int input_h , int input_stride_in_bytes,

+{

+}

+STBIRDEF int stbir_resize_float( const float *input_pixels , int input_w , int input_h , int input_stride_in_bytes,

+{

+}

+STBIRDEF int stbir_resize_uint8_srgb(const unsigned char *input_pixels , int input_w , int input_h , int input_stride_in_bytes,

+{

+}

+STBIRDEF int stbir_resize_uint8_srgb_edgemode(const unsigned char *input_pixels , int input_w , int input_h , int input_stride_in_bytes,

+{

+}

+STBIRDEF int stbir_resize_uint8_generic( const unsigned char *input_pixels , int input_w , int input_h , int input_stride_in_bytes,

+{

+}

+STBIRDEF int stbir_resize_uint16_generic(const stbir_uint16 *input_pixels , int input_w , int input_h , int input_stride_in_bytes,

+{

+}

+STBIRDEF int stbir_resize_float_generic( const float *input_pixels , int input_w , int input_h , int input_stride_in_bytes,

+{

+}

+STBIRDEF int stbir_resize( const void *input_pixels , int input_w , int input_h , int input_stride_in_bytes,

+{

+}

+STBIRDEF int stbir_resize_subpixel(const void *input_pixels , int input_w , int input_h , int input_stride_in_bytes,

+{

+}

+STBIRDEF int stbir_resize_region( const void *input_pixels , int input_w , int input_h , int input_stride_in_bytes,

+{

+}

+#endif // STB_IMAGE_RESIZE_IMPLEMENTATION

+/*

+------------------------------------------------------------------------------

+This software is available under 2 licenses -- choose whichever you prefer.

+------------------------------------------------------------------------------

+ALTERNATIVE A - MIT License

+Copyright (c) 2017 Sean Barrett

+Permission is hereby granted, free of charge, to any person obtaining a copy of

+this software and associated documentation files (the "Software"), to deal in

+the Software without restriction, including without limitation the rights to

+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies

+of the Software, and to permit persons to whom the Software is furnished to do

+so, subject to the following conditions:

+The above copyright notice and this permission notice shall be included in all

+copies or substantial portions of the Software.

+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR

+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,

+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE

+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER

+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,

+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE

+SOFTWARE.

+------------------------------------------------------------------------------

+ALTERNATIVE B - Public Domain (www.unlicense.org)

+This is free and unencumbered software released into the public domain.

+Anyone is free to copy, modify, publish, use, compile, sell, or distribute this

+software, either in source code form or as a compiled binary, for any purpose,

+commercial or non-commercial, and by any means.

+In jurisdictions that recognize copyright laws, the author or authors of this

+software dedicate any and all copyright interest in the software to the public

+domain. We make this dedication for the benefit of the public at large and to

+the detriment of our heirs and successors. We intend this dedication to be an

+overt act of relinquishment in perpetuity of all present and future rights to

+this software under copyright law.

+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR

+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,

+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE

+AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN

+ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION

+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

+------------------------------------------------------------------------------

+*/

diff --git a/src/ui/bindingswidget.c b/src/ui/bindingswidget.c

index 9a2070ba..558bdcd5 100644

--- a/src/ui/bindingswidget.c

+++ b/src/ui/bindingswidget.c

@@ -26,7 +26,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */

#include "command.h"

#include "util.h"

#include "app.h"

-#if defined (iPlatformApple)

+#if defined (iPlatformAppleDesktop)

include "macos.h"

#endif

@@ -140,7 +140,7 @@ static void setActiveItem_BindingsWidget_(iBindingsWidget *d, size_t pos) {

     item->isWaitingForEvent = iTrue;

     invalidateItem_ListWidget(d->list, d->activePos);

 }

-#if defined (iPlatformApple)

+#if defined (iPlatformAppleDesktop)

 /* Native menus must be disabled while grabbing keys so the shortcuts don't trigger. */

 const iBool enableNativeMenus = (d->activePos == iInvalidPos);

 enableMenu_MacOS("Edit", enableNativeMenus);

diff --git a/src/ui/color.c b/src/ui/color.c

index 0227bd3e..51dcdc44 100644

--- a/src/ui/color.c

+++ b/src/ui/color.c

@@ -21,6 +21,7 @@ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT

SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */

#include "color.h"

+#include "app.h"

#include <the_Foundation/string.h>

@@ -52,10 +53,10 @@ static const iColor lightPalette_[] = {

 { 235, 235, 235, 255 },

 { 255, 255, 255, 255 },

 { 255, 255, 32,  255 },

 { 255, 64,  64,  255 },

@@ -71,14 +72,21 @@ iLocalDef void copy_(enum iColorId dst, enum iColorId src) {

}

void setThemePalette_Color(enum iColorTheme theme) {

 memcpy(palette_, isDark_ColorTheme(theme) ? darkPalette_ : lightPalette_, sizeof(darkPalette_));

 switch (theme) {

         copy_(uiBackground_ColorId, black_ColorId);

         copy_(uiBackgroundHover_ColorId, black_ColorId);

         copy_(uiText_ColorId, gray75_ColorId);

         copy_(uiTextPressed_ColorId, black_ColorId);

         copy_(uiTextStrong_ColorId, white_ColorId);

@@ -86,44 +94,48 @@ void setThemePalette_Color(enum iColorTheme theme) {

         copy_(uiTextSelected_ColorId, white_ColorId);

         copy_(uiTextFramelessHover_ColorId, white_ColorId);

         copy_(uiTextDisabled_ColorId, gray25_ColorId);

         copy_(uiFrame_ColorId, black_ColorId);

         copy_(uiEmboss1_ColorId, gray25_ColorId);

         copy_(uiEmboss2_ColorId, black_ColorId);

         copy_(uiEmbossPressed2_ColorId, gray75_ColorId);

         copy_(uiEmbossSelected2_ColorId, black_ColorId);

         copy_(uiEmbossSelectedHover1_ColorId, white_ColorId);

         copy_(uiInputBackground_ColorId, black_ColorId);

         copy_(uiInputBackgroundFocused_ColorId, black_ColorId);

         copy_(uiInputText_ColorId, gray75_ColorId);

         copy_(uiInputTextFocused_ColorId, white_ColorId);

         copy_(uiInputFrame_ColorId, gray25_ColorId);

         copy_(uiInputCursorText_ColorId, black_ColorId);

         copy_(uiSeparator_ColorId, gray25_ColorId);

         break;

     default:

         copy_(uiBackground_ColorId, gray25_ColorId);

         copy_(uiBackgroundHover_ColorId, gray25_ColorId);

         copy_(uiText_ColorId, gray75_ColorId);

         copy_(uiTextPressed_ColorId, black_ColorId);

         copy_(uiTextStrong_ColorId, white_ColorId);

@@ -131,43 +143,48 @@ void setThemePalette_Color(enum iColorTheme theme) {

         copy_(uiTextSelected_ColorId, white_ColorId);

         copy_(uiTextDisabled_ColorId, gray50_ColorId);

         copy_(uiTextFramelessHover_ColorId, white_ColorId);

         copy_(uiFrame_ColorId, gray25_ColorId);

         copy_(uiEmboss1_ColorId, gray50_ColorId);

         copy_(uiEmboss2_ColorId, black_ColorId);

         copy_(uiEmbossPressed2_ColorId, white_ColorId);

         copy_(uiEmbossSelected2_ColorId, black_ColorId);

         copy_(uiEmbossSelectedHover1_ColorId, white_ColorId);

         copy_(uiInputBackgroundFocused_ColorId, black_ColorId);

         copy_(uiInputText_ColorId, gray75_ColorId);

         copy_(uiInputTextFocused_ColorId, white_ColorId);

         copy_(uiInputCursorText_ColorId, black_ColorId);

         copy_(uiSeparator_ColorId, black_ColorId);

         break;

     case light_ColorTheme:

         copy_(uiBackground_ColorId, gray75_ColorId);

         copy_(uiBackgroundHover_ColorId, gray75_ColorId);

         copy_(uiText_ColorId, black_ColorId);

         copy_(uiTextStrong_ColorId, black_ColorId);

         copy_(uiTextDim_ColorId, gray25_ColorId);

@@ -175,9 +192,10 @@ void setThemePalette_Color(enum iColorTheme theme) {

         copy_(uiTextSelected_ColorId, black_ColorId);

         copy_(uiTextDisabled_ColorId, gray50_ColorId);

         copy_(uiTextFramelessHover_ColorId, black_ColorId);

         copy_(uiFrame_ColorId, gray50_ColorId);

         copy_(uiEmboss1_ColorId, white_ColorId);

         copy_(uiEmboss2_ColorId, gray50_ColorId);

@@ -186,32 +204,36 @@ void setThemePalette_Color(enum iColorTheme theme) {

         copy_(uiEmbossPressed1_ColorId, black_ColorId);

         copy_(uiEmbossPressed2_ColorId, white_ColorId);

         copy_(uiEmbossSelected1_ColorId, white_ColorId);

         copy_(uiInputBackground_ColorId, white_ColorId);

         copy_(uiInputBackgroundFocused_ColorId, white_ColorId);

         copy_(uiInputText_ColorId, gray25_ColorId);

         copy_(uiInputTextFocused_ColorId, black_ColorId);

         copy_(uiInputCursorText_ColorId, white_ColorId);

         copy_(uiAnnotation_ColorId, gray50_ColorId);

         break;

     case pureWhite_ColorTheme:

         copy_(uiBackground_ColorId, white_ColorId);

         copy_(uiBackgroundHover_ColorId, gray75_ColorId);

         set_Color(uiText_ColorId,

                   mix_Color(get_Color(black_ColorId), get_Color(gray25_ColorId), 0.5f));

         copy_(uiTextPressed_ColorId, black_ColorId);

@@ -220,18 +242,19 @@ void setThemePalette_Color(enum iColorTheme theme) {

         copy_(uiTextDim_ColorId, gray25_ColorId);

         copy_(uiTextSelected_ColorId, black_ColorId);

         copy_(uiTextFramelessHover_ColorId, black_ColorId);

         copy_(uiFrame_ColorId, gray75_ColorId);

         copy_(uiEmboss1_ColorId, white_ColorId);

         copy_(uiEmboss2_ColorId, white_ColorId);

         copy_(uiEmbossHover1_ColorId, gray25_ColorId);

         copy_(uiEmbossHover2_ColorId, gray25_ColorId);

         copy_(uiEmbossPressed1_ColorId, black_ColorId);

         copy_(uiEmbossSelected1_ColorId, white_ColorId);

         copy_(uiEmbossSelectedHover1_ColorId, gray50_ColorId);

         copy_(uiEmbossSelectedHover2_ColorId, gray50_ColorId);

         copy_(uiInputBackground_ColorId, white_ColorId);

@@ -239,31 +262,33 @@ void setThemePalette_Color(enum iColorTheme theme) {

         copy_(uiInputText_ColorId, gray25_ColorId);

         copy_(uiInputTextFocused_ColorId, black_ColorId);

         copy_(uiInputFrame_ColorId, gray50_ColorId);

         copy_(uiInputCursorText_ColorId, white_ColorId);

         copy_(uiAnnotation_ColorId, gray50_ColorId);

         break;

 }

 set_Color(uiSubheading_ColorId,

           mix_Color(get_Color(uiText_ColorId),

                     get_Color(uiIcon_ColorId),

                     isDark_ColorTheme(theme) ? 0.5f : 0.75f));

 setHsl_Color(uiBackgroundFolder_ColorId,

                                 0,

                                                               : -0.075));

 palette_[uiMarked_ColorId].a = 128;

 palette_[uiMatching_ColorId].a = 128;

@@ -423,8 +448,12 @@ const char *escape_Color(int color) {

 if (color >= 0 && color < (int) iElemCount(esc)) {

     return esc[color];

 }

}

iHSLColor setSat_HSLColor(iHSLColor d, float sat) {

diff --git a/src/ui/color.h b/src/ui/color.h

index 6849ff2b..8556cd72 100644

--- a/src/ui/color.h

+++ b/src/ui/color.h

@@ -33,6 +33,12 @@ enum iColorTheme {

 max_ColorTheme

};

+enum iColorAccent {

+};

iLocalDef iBool isDark_ColorTheme(enum iColorTheme d) {

 return d == pureBlack_ColorTheme || d == dark_ColorTheme;

}

@@ -106,6 +112,8 @@ enum iColorId {

 uiBackgroundFolder_ColorId,

 uiTextDim_ColorId,

 uiSubheading_ColorId,

 /* content theme colors */

 tmFirst_ColorId,

@@ -172,7 +180,9 @@ iLocalDef iBool isRegularText_ColorId(enum iColorId d) {

#define permanent_ColorId 0x80 /* cannot be changed via escapes */

#define asciiBase_ColorEscape 33

+#define asciiExtended_ColorEscape (128 - asciiBase_ColorEscape)

+#define restore_ColorEscape "\r\x24" /* ASCII Cancel */

#define black_ColorEscape "\r!"

#define gray25_ColorEscape "\r""

#define gray50_ColorEscape "\r#"

diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c

index ef8d5ff3..947d9d5f 100644

--- a/src/ui/documentwidget.c

+++ b/src/ui/documentwidget.c

@@ -41,7 +41,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */

#include "labelwidget.h"

#include "media.h"

#include "paint.h"

-#include "playerui.h"

+#include "mediaui.h"

#include "scrollwidget.h"

#include "util.h"

#include "visbuf.h"

@@ -67,14 +67,49 @@ iDeclareType(PersistentDocumentState)

iDeclareTypeConstruction(PersistentDocumentState)

iDeclareTypeSerialization(PersistentDocumentState)

+enum iReloadInterval {

+};

+static int seconds_ReloadInterval_(enum iReloadInterval d) {

+}

+static const char *label_ReloadInterval_(enum iReloadInterval d) {

+}

struct Impl_PersistentDocumentState {

 iHistory *history;

 iString * url;

};

void init_PersistentDocumentState(iPersistentDocumentState *d) {

}

void deinit_PersistentDocumentState(iPersistentDocumentState *d) {

@@ -84,7 +119,7 @@ void deinit_PersistentDocumentState(iPersistentDocumentState *d) {

void serialize_PersistentDocumentState(const iPersistentDocumentState *d, iStream *outs) {

 serialize_String(d->url, outs);

 serialize_History(d->history, outs);

}

@@ -94,7 +129,8 @@ void deserialize_PersistentDocumentState(iPersistentDocumentState *d, iStream *i

     /* Oopsie, this should not have been written; invalid URL. */

     clear_String(d->url);

 }

 deserialize_History(d->history, ins);

}

@@ -102,17 +138,7 @@ iDefineTypeConstruction(PersistentDocumentState)

/----------------------------------------------------------------------------------------------/

-iDeclareType(OutlineItem)

-struct Impl_OutlineItem {

-};

-/----------------------------------------------------------------------------------------------/

-static void animatePlayers_DocumentWidget_ (iDocumentWidget *d);

+static void animateMedia_DocumentWidget_ (iDocumentWidget *d);

static void updateSideIconBuf_DocumentWidget_ (iDocumentWidget *d);

static const int smoothDuration_DocumentWidget_ = 600; /* milliseconds */

@@ -133,6 +159,7 @@ enum iDocumentWidgetFlag {

 showLinkNumbers_DocumentWidgetFlag       = iBit(3),

 setHoverViaKeys_DocumentWidgetFlag       = iBit(4),

 newTabViaHomeKeys_DocumentWidgetFlag     = iBit(5),

};

enum iDocumentLinkOrdinalMode {

@@ -151,6 +178,7 @@ struct Impl_DocumentWidget {

 iGmRequest *   request;

 iAtomicInt     isRequestUpdated; /* request has new content, need to parse it */

 iObjectList *  media;

 iString        sourceHeader;

 iString        sourceMime;

 iBlock         sourceContent; /* original content as received, for saving */

@@ -170,10 +198,10 @@ struct Impl_DocumentWidget {

 iAnim          animWideRunOffset;

 uint16_t       animWideRunId;

 iGmRunRange    animWideRunRange;

 const iGmRun * grabbedPlayer; /* currently adjusting volume in a player */

 float          grabbedStartVolume;

 const iGmRun * hoverLink;

 const iGmRun * contextLink;

 const iGmRun * firstVisibleRun;

@@ -183,8 +211,6 @@ struct Impl_DocumentWidget {

 float          initNormScrollY;

 iAnim          scrollY;

 iAnim          sideOpacity;

 iScrollWidget *scroll;

 iWidget *      menu;

 iWidget *      playerMenu;

@@ -228,9 +254,8 @@ void init_DocumentWidget(iDocumentWidget *d) {

 d->lastVisibleRun   = NULL;

 d->visBuf           = new_VisBuf();

 d->invalidRuns      = new_PtrSet();

 init_Anim(&d->sideOpacity, 0);

 init_String(&d->sourceHeader);

 init_String(&d->sourceMime);

 init_Block(&d->sourceContent, 0);

@@ -238,9 +263,9 @@ void init_DocumentWidget(iDocumentWidget *d) {

 init_PtrArray(&d->visibleLinks);

 init_PtrArray(&d->visibleWideRuns);

 init_Array(&d->wideRunOffsets, sizeof(int));

 d->grabbedPlayer = NULL;

 init_String(&d->pendingGotoHeading);

 init_Click(&d->click, d, SDL_BUTTON_LEFT);

 addChild_Widget(w, iClob(d->scroll = new_ScrollWidget()));

@@ -251,7 +276,7 @@ void init_DocumentWidget(iDocumentWidget *d) {

 addChildFlags_Widget(w,

                      iClob(new_IndicatorWidget()),

                      resizeToParentWidth_WidgetFlag | resizeToParentHeight_WidgetFlag);

-#if !defined (iPlatformApple) /* in system menu */

+#if !defined (iPlatformAppleDesktop) /* in system menu */

 addAction_Widget(w, reload_KeyShortcut, "navigate.reload");

 addAction_Widget(w, closeTab_KeyShortcut, "tabs.close");

 addAction_Widget(w, SDLK_d, KMOD_PRIMARY, "bookmark.add");

@@ -270,7 +295,6 @@ void deinit_DocumentWidget(iDocumentWidget *d) {

 delete_TextBuf(d->timestampBuf);

 delete_VisBuf(d->visBuf);

 delete_PtrSet(d->invalidRuns);

 iRelease(d->media);

 iRelease(d->request);

 deinit_String(&d->pendingGotoHeading);

@@ -278,11 +302,11 @@ void deinit_DocumentWidget(iDocumentWidget *d) {

 deinit_String(&d->sourceMime);

 deinit_String(&d->sourceHeader);

 iRelease(d->doc);

 }

 deinit_Array(&d->wideRunOffsets);

 deinit_PtrArray(&d->visibleWideRuns);

 deinit_PtrArray(&d->visibleLinks);

 delete_Block(d->certFingerprint);

@@ -312,10 +336,14 @@ static void requestFinished_DocumentWidget_(iAnyObject *obj) {

}

static int documentWidth_DocumentWidget_(const iDocumentWidget *d) {

              fontSize_UI * prefs->lineWidth * prefs->zoomPercent / 100);

}

@@ -332,15 +360,17 @@ static iRect documentBounds_DocumentWidget_(const iDocumentWidget *d) {

     rect.pos.y += margin;

     rect.size.y -= margin;

 }

 }

 return rect;

}

@@ -369,7 +399,7 @@ static iRangei visibleRange_DocumentWidget_(const iDocumentWidget *d) {

static void addVisible_DocumentWidget_(void *context, const iGmRun *run) {

 iDocumentWidget *d = context;

     if (!d->firstVisibleRun) {

         d->firstVisibleRun = run;

     }

@@ -378,8 +408,9 @@ static void addVisible_DocumentWidget_(void *context, const iGmRun *run) {

 if (run->preId && run->flags & wide_GmRunFlag) {

     pushBack_PtrArray(&d->visibleWideRuns, run);

 }

 }

 if (run->linkId) {

     pushBack_PtrArray(&d->visibleLinks, run);

@@ -488,7 +519,7 @@ static void updateHover_DocumentWidget_(iDocumentWidget *d, iInt2 mouse) {

static void animate_DocumentWidget_(void *ticker) {

 iDocumentWidget *d = ticker;

     addTicker_App(animate_DocumentWidget_, d);

 }

}

@@ -503,71 +534,66 @@ static void updateSideOpacity_DocumentWidget_(iDocumentWidget *d, iBool isAnimat

 animate_DocumentWidget_(d);

}

-static void updateOutlineOpacity_DocumentWidget_(iDocumentWidget *d) {

-}

-static uint32_t playerUpdateInterval_DocumentWidget_(const iDocumentWidget *d) {

+static uint32_t mediaUpdateInterval_DocumentWidget_(const iDocumentWidget *d) {

 if (document_App() != d) {

     return 0;

 }

     const iGmRun *run = i.ptr;

     }

 }

}

-static uint32_t postPlayerUpdate_DocumentWidget_(uint32_t interval, void *context) {

+static uint32_t postMediaUpdate_DocumentWidget_(uint32_t interval, void *context) {

 /* Called in timer thread; don't access the widget. */

 iUnused(context);

 postCommand_App("media.player.update");

 return interval;

}

-static void updatePlayers_DocumentWidget_(iDocumentWidget *d) {

+static void updateMedia_DocumentWidget_(iDocumentWidget *d) {

 if (document_App() == d) {

     refresh_Widget(d);

         const iGmRun *run = i.ptr;

         }

     }

 }

 }

}

-static void animatePlayers_DocumentWidget_(iDocumentWidget *d) {

+static void animateMedia_DocumentWidget_(iDocumentWidget *d) {

 if (document_App() != d) {

     }

     return;

 }

 }

}

@@ -590,6 +616,10 @@ static iRangecc currentHeading_DocumentWidget_(const iDocumentWidget *d) {

}

static void updateVisible_DocumentWidget_(iDocumentWidget *d) {

 const iRangei visRange = visibleRange_DocumentWidget_(d);

 const iRect   bounds   = bounds_Widget(as_Widget(d));

 setRange_ScrollWidget(d->scroll, (iRangei){ 0, scrollMax_DocumentWidget_(d) });

@@ -599,7 +629,7 @@ static void updateVisible_DocumentWidget_(iDocumentWidget *d) {

                       docSize > 0 ? height_Rect(bounds) * size_Range(&visRange) / docSize : 0);

 clear_PtrArray(&d->visibleLinks);

 clear_PtrArray(&d->visibleWideRuns);

 const iRangecc oldHeading = currentHeading_DocumentWidget_(d);

 /* Scan for visible runs. */ {

     d->firstVisibleRun = NULL;

@@ -611,7 +641,7 @@ static void updateVisible_DocumentWidget_(iDocumentWidget *d) {

 }

 updateHover_DocumentWidget_(d, mouseCoord_Window(get_Window()));

 updateSideOpacity_DocumentWidget_(d, iTrue);

 /* Remember scroll positions of recently visited pages. */ {

     iRecentUrl *recent = mostRecentUrl_History(d->mod.history);

     if (recent && docSize && d->state == ready_RequestState) {

@@ -637,7 +667,9 @@ static void updateWindowTitle_DocumentWidget_(const iDocumentWidget *d) {

     iUrl parts;

     init_Url(&parts, d->mod.url);

     if (equalCase_Rangecc(parts.scheme, "about")) {

     }

     else if (!isEmpty_Range(&parts.host)) {

         pushBackRange_StringArray(title, parts.host);

@@ -659,9 +691,10 @@ static void updateWindowTitle_DocumentWidget_(const iDocumentWidget *d) {

     const iChar siteIcon = siteIcon_GmDocument(d->doc);

     if (siteIcon) {

         if (!isEmpty_String(text)) {

         }

         prependChar_String(text, siteIcon);

     }

     const int width = advanceRange_Text(default_FontId, range_String(text)).x;

     if (width <= avail ||

@@ -703,69 +736,27 @@ static void invalidate_DocumentWidget_(iDocumentWidget *d) {

 clear_PtrSet(d->invalidRuns);

}

-static int outlineWidth_DocumentWidget_(const iDocumentWidget *d) {

-}

static iRangecc bannerText_DocumentWidget_(const iDocumentWidget *d) {

 return isEmpty_String(d->titleUser) ? range_String(bannerText_GmDocument(d->doc))

                                     : range_String(d->titleUser);

}

-static void updateOutline_DocumentWidget_(iDocumentWidget *d) {

-// const iRangecc topText = urlHost_String(d->mod.url);

-// iInt2 size = advanceWrapRange_Text(uiContent_FontId, outWidth, topText);

-// pushBack_Array(&d->outline, &(iOutlineItem){ topText, uiContent_FontId, (iRect){ pos, size },

-// tmBannerTitle_ColorId, none_ColorId });

-// pos.y += size.y;

-}

-static void setSource_DocumentWidget_(iDocumentWidget *d, const iString *source) {

+static void documentRunsInvalidated_DocumentWidget_(iDocumentWidget *d) {

 d->foundMark       = iNullRange;

 d->selectMark      = iNullRange;

 d->hoverLink       = NULL;

 d->contextLink     = NULL;

 d->firstVisibleRun = NULL;

 d->lastVisibleRun  = NULL;

+}

+static void setSource_DocumentWidget_(iDocumentWidget *d, const iString *source) {

 updateWindowTitle_DocumentWidget_(d);

 updateVisible_DocumentWidget_(d);

 updateSideIconBuf_DocumentWidget_(d);

 invalidate_DocumentWidget_(d);

 refresh_Widget(as_Widget(d));

}

@@ -1001,7 +992,7 @@ static void updateTrust_DocumentWidget_(iDocumentWidget *d, const iGmResponse *r

 else if (d->certFlags & trusted_GmCertFlag) {

     updateTextCStr_LabelWidget(lock, green_ColorEscape closedLock_CStr);

 }

     updateTextCStr_LabelWidget(lock, isDarkMode ? orange_ColorEscape "\u26a0"

         : black_ColorEscape "\u26a0");

 }

@@ -1009,17 +1000,7 @@ static void updateTrust_DocumentWidget_(iDocumentWidget *d, const iGmResponse *r

}

static void parseUser_DocumentWidget_(iDocumentWidget *d) {

}

static iBool updateFromHistory_DocumentWidget_(iDocumentWidget *d) {

@@ -1034,6 +1015,7 @@ static iBool updateFromHistory_DocumentWidget_(iDocumentWidget *d) {

     /* Use the cached response data. */

     updateTrust_DocumentWidget_(d, resp);

     d->sourceTime = resp->when;

     format_String(&d->sourceHeader, "(cached content)");

     updateTimestampBuf_DocumentWidget_(d);

     set_Block(&d->sourceContent, &resp->body);

@@ -1042,7 +1024,6 @@ static iBool updateFromHistory_DocumentWidget_(iDocumentWidget *d) {

     d->state = ready_RequestState;

     updateSideOpacity_DocumentWidget_(d, iFalse);

     updateSideIconBuf_DocumentWidget_(d);

     updateVisible_DocumentWidget_(d);

     postCommandf_App("document.changed doc:%p url:%s", d, cstr_String(d->mod.url));

     return iTrue;

@@ -1109,6 +1090,9 @@ static void scroll_DocumentWidget_(iDocumentWidget *d, int offset) {

}

static void scrollTo_DocumentWidget_(iDocumentWidget *d, int documentY, iBool centered) {

 init_Anim(&d->scrollY,

           documentY - (centered ? documentBounds_DocumentWidget_(d).size.y / 2

                                 : lineHeight_Text(paragraph_FontId)));

@@ -1188,6 +1172,7 @@ static void checkResponse_DocumentWidget_(iDocumentWidget *d) {

     updateTrust_DocumentWidget_(d, resp);

     init_Anim(&d->sideOpacity, 0);

     format_String(&d->sourceHeader, "%d %s", statusCode, get_GmError(statusCode)->title);

     switch (category_GmStatusCode(statusCode)) {

         case categoryInput_GmStatusCode: {

             iUrl parts;

@@ -1323,16 +1308,20 @@ static iMediaRequest *findMediaRequest_DocumentWidget_(const iDocumentWidget *d,

 return NULL;

}

-static iBool requestMedia_DocumentWidget_(iDocumentWidget *d, iGmLinkId linkId) {

+static iBool requestMedia_DocumentWidget_(iDocumentWidget *d, iGmLinkId linkId, iBool enableFilters) {

 if (!findMediaRequest_DocumentWidget_(d, linkId)) {

     invalidate_DocumentWidget_(d);

     return iTrue;

 }

 return iFalse;

}

+static iBool isDownloadRequest_DocumentWidget(const iDocumentWidget *d, const iMediaRequest *req) {

+}

static iBool handleMediaCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) {

 iMediaRequest *req = pointerLabel_Command(cmd, "request");

 iBool isOurRequest = iFalse;

@@ -1351,7 +1340,8 @@ static iBool handleMediaCommand_DocumentWidget_(iDocumentWidget *d, const char *

     const enum iGmStatusCode code = status_GmRequest(req->req);

     if (isSuccess_GmStatusCode(code)) {

         iGmResponse *resp = lockResponse_GmRequest(req->req);

             /* TODO: Use a helper? This is same as below except for the partialData flag. */

             if (setData_Media(media_GmDocument(d->doc),

                               req->linkId,

@@ -1375,7 +1365,8 @@ static iBool handleMediaCommand_DocumentWidget_(iDocumentWidget *d, const char *

     const enum iGmStatusCode code = status_GmRequest(req->req);

     /* Give the media to the document for presentation. */

     if (isSuccess_GmStatusCode(code)) {

             startsWith_String(meta_GmRequest(req->req), "audio/")) {

             setData_Media(media_GmDocument(d->doc),

                           req->linkId,

@@ -1413,12 +1404,13 @@ static void allocVisBuffer_DocumentWidget_(const iDocumentWidget *d) {

static iBool fetchNextUnfetchedImage_DocumentWidget_(iDocumentWidget *d) {

 iConstForEach(PtrArray, i, &d->visibleLinks) {

     const iGmRun *run = i.ptr;

         const int linkFlags = linkFlags_GmDocument(d->doc, run->linkId);

         if (isMediaLink_GmDocument(d->doc, run->linkId) &&

             linkFlags & imageFileExtension_GmLinkFlag &&

             ~linkFlags & content_GmLinkFlag && ~linkFlags & permanent_GmLinkFlag ) {

                 return iTrue;

             }

         }

@@ -1428,57 +1420,7 @@ static iBool fetchNextUnfetchedImage_DocumentWidget_(iDocumentWidget *d) {

}

static void saveToDownloads_(const iString *url, const iString *mime, const iBlock *content) {

 /* Write the file. */ {

     iFile *f = new_File(savePath);

     if (open_File(f, writeOnly_FileMode)) {

@@ -1496,7 +1438,6 @@ static void saveToDownloads_(const iString *url, const iString *mime, const iBlo

     }

     iRelease(f);

 }

}

static void addAllLinks_(void *context, const iGmRun *run) {

@@ -1534,30 +1475,45 @@ static const int homeRowKeys_[] = {

 't', 'y',

};

+static void updateDocumentWidthRetainingScrollPosition_DocumentWidget_(iDocumentWidget *d,

+}

static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) {

 iWidget *w = as_Widget(d);

 if (equal_Command(cmd, "window.resized") || equal_Command(cmd, "font.changed")) {

     /* Alt/Option key may be involved in window size changes. */

     iChangeFlags(d->flags, showLinkNumbers_DocumentWidgetFlag, iFalse);

     updateSideIconBuf_DocumentWidget_(d);

     invalidate_DocumentWidget_(d);

     dealloc_VisBuf(d->visBuf);

     updateWindowTitle_DocumentWidget_(d);

@@ -1572,11 +1528,11 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)

     return iFalse;

 }

 else if (equal_Command(cmd, "window.mouse.exited")) {

     return iFalse;

 }

 else if (equal_Command(cmd, "theme.changed") && document_App() == d) {

     updateTheme_DocumentWidget_(d);

     updateTrust_DocumentWidget_(d, NULL);

     updateSideIconBuf_DocumentWidget_(d);

     invalidate_DocumentWidget_(d);

@@ -1596,10 +1552,9 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)

     }

     init_Anim(&d->sideOpacity, 0);

     updateSideOpacity_DocumentWidget_(d, iFalse);

     updateWindowTitle_DocumentWidget_(d);

     allocVisBuffer_DocumentWidget_(d);

     return iFalse;

 }

 else if (equal_Command(cmd, "tab.created")) {

@@ -1608,10 +1563,8 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)

     return iFalse;

 }

 else if (equal_Command(cmd, "document.info") && d == document_App()) {

     const iBool haveFingerprint = (d->certFlags & haveFingerprint_GmCertFlag) != 0;

     const iBool canTrust =

         (d->certFlags == (available_GmCertFlag | haveFingerprint_GmCertFlag |

@@ -1655,15 +1608,29 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)

                   uiText_ColorEscape,

                   d->certFlags & trusted_GmCertFlag ? "Trusted" : "Not trusted");

     setFocus_Widget(NULL);

     iWidget *dlg = makeQuestion_Widget(uiHeading_ColorEscape "PAGE INFORMATION",

                                        cstr_String(msg),

     /* Enforce a minimum size. */

     iWidget *sizer = new_Widget();

     setSize_Widget(sizer, init_I2(gap_UI * 90, 1));

     addChildFlags_Widget(dlg, iClob(sizer), frameless_WidgetFlag);

     arrange_Widget(dlg);

     addAction_Widget(dlg, SDLK_ESCAPE, 0, "message.ok");

     addAction_Widget(dlg, SDLK_SPACE, 0, "message.ok");

@@ -1673,7 +1640,7 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)

     const iRangecc host = urlHost_String(d->mod.url);

     if (!isEmpty_Block(d->certFingerprint) && !isEmpty_Range(&host)) {

         setTrusted_GmCerts(certs_App(), host, d->certFingerprint, &d->certExpiry);

         postCommand_App("document.info");

         updateTrust_DocumentWidget_(d, NULL);

         redoLayout_GmDocument(d->doc);

@@ -1713,6 +1680,19 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)

     }

     return iTrue;

 }

 else if (equal_Command(cmd, "document.input.submit") && document_Command(cmd) == d) {

     iString *value = suffix_Command(cmd, "value");

     set_String(value, collect_String(urlEncode_String(value)));

@@ -1769,7 +1749,6 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)

     iReleasePtr(&d->request);

     updateVisible_DocumentWidget_(d);

     updateSideIconBuf_DocumentWidget_(d);

     postCommandf_App("document.changed doc:%p url:%s", d, cstr_String(d->mod.url));

     /* Check for a pending goto. */

     if (!isEmpty_String(&d->pendingGotoHeading)) {

@@ -1794,7 +1773,7 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)

     }

 }

 else if (equal_Command(cmd, "media.player.update")) {

     return iFalse;

 }

 else if (equal_Command(cmd, "document.stop") && document_App() == d) {

@@ -1828,7 +1807,7 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)

     }

     return iTrue;

 }

     d->initNormScrollY = normScrollPos_DocumentWidget_(d);

     fetch_DocumentWidget_(d);

     return iTrue;

@@ -1904,11 +1883,7 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)

     return iTrue;

 }

 else if (equal_Command(cmd, "navigate.root") && document_App() == d) {

     return iTrue;

 }

 else if (equalWidget_Command(cmd, w, "scroll.moved")) {

@@ -2033,10 +2008,10 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)

             makeQuestion_Widget(

                 uiHeading_ColorEscape "IMPORT BOOKMARKS",

                 format_CStr("Found %d new link%s on the page.", size_PtrArray(links), plural),

                 2);

         }

         else {

@@ -2060,24 +2035,45 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)

 else if (equalWidget_Command(cmd, w, "menu.closed")) {

     updateHover_DocumentWidget_(d, mouseCoord_Window(get_Window()));

 }

 return iFalse;

}

-#if 0

-static int outlineHeight_DocumentWidget_(const iDocumentWidget *d) {

-}

-#endif

-static iRect playerRect_DocumentWidget_(const iDocumentWidget *d, const iGmRun *run) {

+static iRect runRect_DocumentWidget_(const iDocumentWidget *d, const iGmRun *run) {

 const iRect docBounds = documentBounds_DocumentWidget_(d);

 return moved_Rect(run->bounds, addY_I2(topLeft_Rect(docBounds), -value_Anim(&d->scrollY)));

}

static void setGrabbedPlayer_DocumentWidget_(iDocumentWidget *d, const iGmRun *run) {

     setFlags_Player(plr, volumeGrabbed_PlayerFlag, iTrue);

     d->grabbedStartVolume = volume_Player(plr);

     d->grabbedPlayer      = run;

@@ -2085,7 +2081,7 @@ static void setGrabbedPlayer_DocumentWidget_(iDocumentWidget *d, const iGmRun *r

 }

 else if (d->grabbedPlayer) {

     setFlags_Player(

         volumeGrabbed_PlayerFlag,

         iFalse);

     d->grabbedPlayer = NULL;

@@ -2096,7 +2092,7 @@ static void setGrabbedPlayer_DocumentWidget_(iDocumentWidget *d, const iGmRun *r

 }

}

-static iBool processPlayerEvents_DocumentWidget_(iDocumentWidget *d, const SDL_Event *ev) {

+static iBool processMediaEvents_DocumentWidget_(iDocumentWidget *d, const SDL_Event *ev) {

 if (ev->type != SDL_MOUSEBUTTONDOWN && ev->type != SDL_MOUSEBUTTONUP &&

     ev->type != SDL_MOUSEMOTION) {

     return iFalse;

@@ -2111,10 +2107,13 @@ static iBool processPlayerEvents_DocumentWidget_(iDocumentWidget *d, const SDL_E

     return iFalse;

 }

 const iInt2 mouse = init_I2(ev->button.x, ev->button.y);

     const iGmRun *run  = i.ptr;

     if (contains_Rect(rect, mouse)) {

         iPlayerUI ui;

         init_PlayerUI(&ui, plr, rect);

@@ -2135,7 +2134,7 @@ static iBool processPlayerEvents_DocumentWidget_(iDocumentWidget *d, const SDL_E

         }

         if (contains_Rect(ui.playPauseRect, mouse)) {

             setPaused_Player(plr, !isPaused_Player(plr));

             return iTrue;

         }

         else if (contains_Rect(ui.rewindRect, mouse)) {

@@ -2151,7 +2150,7 @@ static iBool processPlayerEvents_DocumentWidget_(iDocumentWidget *d, const SDL_E

             setFlags_Player(plr,

                             adjustingVolume_PlayerFlag,

                             !(flags_Player(plr) & adjustingVolume_PlayerFlag));

             refresh_Widget(d);

             return iTrue;

         }

@@ -2313,6 +2312,8 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e

     }

 }

 else if (ev->type == SDL_MOUSEWHEEL && isHover_Widget(w)) {

     const iInt2 mouseCoord = mouseCoord_Window(get_Window());

#if defined (iPlatformApple)

     /* On macOS, we handle both trackpad and mouse events. We expect SDL to identify

@@ -2320,6 +2321,11 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e

     if (ev->wheel.which == 0) { /* Trackpad with precise scrolling w/inertia. */

         stop_Anim(&d->scrollY);

         iInt2 wheel = init_I2(ev->wheel.x, ev->wheel.y);

+# if defined (iPlatformAppleMobile)

+# else

         /* Only scroll on one axis at a time. */

         if (iAbs(wheel.x) > iAbs(wheel.y)) {

             wheel.y = 0;

@@ -2327,8 +2333,9 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e

         else {

             wheel.x = 0;

         }

+# endif

     }

     else

#endif

@@ -2372,7 +2379,6 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e

     else {

         updateHover_DocumentWidget_(d, mpos);

     }

 }

 if (ev->type == SDL_MOUSEBUTTONDOWN) {

     if (ev->button.button == SDL_BUTTON_X1) {

@@ -2384,7 +2390,8 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e

         return iTrue;

     }

     if (ev->button.button == SDL_BUTTON_MIDDLE && d->hoverLink) {

                          cstr_String(linkUrl_GmDocument(d->doc, d->hoverLink->linkId)));

         return iTrue;

     }

@@ -2399,11 +2406,14 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e

             init_Array(&items, sizeof(iMenuItem));

             if (d->contextLink) {

                 const iString *linkUrl  = linkUrl_GmDocument(d->doc, d->contextLink->linkId);

                 const iRangecc scheme   = urlScheme_String(linkUrl);

                 const iBool    isGemini = equalCase_Rangecc(scheme, "gemini");

                 if (willUseProxy_App(scheme) || isGemini ||

                     equalCase_Rangecc(scheme, "finger") ||

                     equalCase_Rangecc(scheme, "gopher")) {

                     /* Regular links that we can open. */

                     pushBackN_Array(

                         &items,

@@ -2447,10 +2457,18 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e

                                                  0,

                                                  format_CStr("!bookmark.add title:%s url:%s",

                                                              cstr_String(linkLabel),

                                 3);

                 iMediaRequest *mediaReq;

                     if (isFinished_GmRequest(mediaReq->req)) {

                         pushBack_Array(&items,

                                        &(iMenuItem){ "Save to Downloads",

@@ -2477,6 +2495,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e

                         { "Go to Root", navigateRoot_KeyShortcut, "navigate.root" },

                         { "---", 0, 0, NULL },

                         { "Reload Page", reload_KeyShortcut, "navigate.reload" },

                         { "---", 0, 0, NULL },

                         { "Bookmark Page...", SDLK_d, KMOD_PRIMARY, "bookmark.add" },

                         { "Subscribe to Page...", subscribeToPage_KeyModifier, "feeds.subscribe" },

@@ -2500,7 +2519,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e

         processContextMenuEvent_Widget(d->menu, ev, {});

     }

 }

     return iTrue;

 }

 /* The left mouse button. */

@@ -2511,9 +2530,9 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e

     case drag_ClickResult: {

         if (d->grabbedPlayer) {

             iPlayer *plr =

             iPlayerUI ui;

             float off = (float) delta_Click(&d->click).x / (float) width_Rect(ui.volumeSlider);

             setVolume_Player(plr, d->grabbedStartVolume + off);

             refresh_Widget(w);

@@ -2557,13 +2576,13 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e

                 const int linkFlags = linkFlags_GmDocument(d->doc, linkId);

                 iAssert(linkId);

                 /* Media links are opened inline by default. */

                     if (linkFlags & content_GmLinkFlag && linkFlags & permanent_GmLinkFlag) {

                         /* We have the content and it cannot be dismissed, so nothing

                            further to do. */

                         return iTrue;

                     }

                         if (linkFlags & content_GmLinkFlag) {

                             /* Dismiss shown content on click. */

                             setData_Media(media_GmDocument(d->doc),

@@ -2622,9 +2641,10 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e

                             "Open this link in the default browser?\n" uiTextAction_ColorEscape

                             "%s",

                             cstr_String(url)),

                         2);

                 }

             }

@@ -2717,7 +2737,7 @@ static void fillRange_DrawContext_(iDrawContext *d, const iGmRun *run, enum iCol

static void drawMark_DrawContext_(void *context, const iGmRun *run) {

 iDrawContext *d = context;

     fillRange_DrawContext_(d, run, uiMatching_ColorId, d->widget->foundMark, &d->inFoundMark);

     fillRange_DrawContext_(d, run, uiMarked_ColorId, d->widget->selectMark, &d->inSelectMark);

 }

@@ -2816,8 +2836,8 @@ static void drawBannerRun_DrawContext_(iDrawContext *d, const iGmRun *run, iInt2

static void drawRun_DrawContext_(void *context, const iGmRun *run) {

 iDrawContext *d      = context;

 const iInt2   origin = d->viewPos;

     if (tex) {

         const iRect dst = moved_Rect(run->visBounds, origin);

         fillRect_Paint(&d->paint, dst, tmBackground_ColorId); /* in case the image has alpha */

@@ -2826,8 +2846,8 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) {

     }

     return;

 }

     return;

 }

 enum iColorId      fg  = run->color;

@@ -2847,12 +2867,10 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) {

 }

 if (run->flags & siteBanner_GmRunFlag) {

     /* Banner background. */

     drawBannerRun_DrawContext_(d, run, visPos);

 }

 else {

@@ -2893,18 +2911,26 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) {

         init_String(&text);

         iMediaId imageId = linkImage_GmDocument(doc, run->linkId);

         iMediaId audioId = !imageId ? linkAudio_GmDocument(doc, run->linkId) : 0;

         if (imageId) {

             iAssert(!isEmpty_Rect(run->bounds));

             imageInfo_Media(constMedia_GmDocument(doc), imageId, &info);

             format_String(&text, "%s \u2014 %d x %d \u2014 %.1fMB",

         }

         else if (audioId) {

             audioInfo_Media(constMedia_GmDocument(doc), audioId, &info);

         }

         if (findMediaRequest_DocumentWidget_(d->widget, run->linkId)) {

             appendFormat_String(

@@ -3032,11 +3058,11 @@ static void updateSideIconBuf_DocumentWidget_(iDocumentWidget *d) {

 if (!banner) {

     return;

 }

 /* Determine the required size. */

 iInt2 bufSize = init1_I2(minBannerSize);

 if (isHeadingVisible) {

@@ -3113,74 +3139,24 @@ static void drawSideElements_DocumentWidget_(const iDocumentWidget *d) {

                         iMax(0, scrollMax_DocumentWidget_(d) - value_Anim(&d->scrollY)))),

         tmQuoteIcon_ColorId);

 }

-#if 0

-#endif

 unsetClip_Paint(&p);

}

-static void drawPlayers_DocumentWidget_(const iDocumentWidget *d, iPaint *p) {

+static void drawMedia_DocumentWidget_(const iDocumentWidget *d, iPaint *p) {

     const iGmRun * run = i.ptr;

 }

}

@@ -3274,7 +3250,7 @@ static void draw_DocumentWidget_(const iDocumentWidget *d) {

     render_GmDocument(d->doc, vis, drawMark_DrawContext_, &ctx);

     SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_NONE);

 }

 unsetClip_Paint(&ctx.paint);

 /* Fill the top and bottom, in case the document is short. */

 if (yTop > top_Rect(bounds)) {

@@ -3299,6 +3275,9 @@ static void draw_DocumentWidget_(const iDocumentWidget *d) {

     fillRect_Paint(&ctx.paint, linkRect, tmBackground_ColorId);

     drawRange_Text(font, addX_I2(topLeft_Rect(linkRect), gap_UI), tmParagraph_ColorId, linkUrl);

 }

 draw_Widget(w);

}

@@ -3320,6 +3299,10 @@ const iBlock *sourceContent_DocumentWidget(const iDocumentWidget *d) {

 return &d->sourceContent;

}

+int documentWidth_DocumentWidget(const iDocumentWidget *d) {

+}

const iString *feedTitle_DocumentWidget(const iDocumentWidget *d) {

 if (!isEmpty_String(title_GmDocument(d->doc))) {

     return title_GmDocument(d->doc);

@@ -3394,10 +3377,9 @@ iBool isRequestOngoing_DocumentWidget(const iDocumentWidget *d) {

}

void updateSize_DocumentWidget(iDocumentWidget *d) {

 resetWideRuns_DocumentWidget_(d);

 updateSideIconBuf_DocumentWidget_(d);

 updateVisible_DocumentWidget_(d);

 invalidate_DocumentWidget_(d);

}

diff --git a/src/ui/documentwidget.h b/src/ui/documentwidget.h

index 91337a24..f922e7ea 100644

--- a/src/ui/documentwidget.h

+++ b/src/ui/documentwidget.h

@@ -43,6 +43,7 @@ const iBlock * sourceContent_DocumentWidget (const iDocumentWidget *);

const iGmDocument * document_DocumentWidget (const iDocumentWidget *);

const iString * bookmarkTitle_DocumentWidget (const iDocumentWidget *);

const iString * feedTitle_DocumentWidget (const iDocumentWidget *);

+int documentWidth_DocumentWidget (const iDocumentWidget *);

void setUrl_DocumentWidget (iDocumentWidget *, const iString *url);

void setUrlFromCache_DocumentWidget (iDocumentWidget *, const iString *url, iBool isFromCache);

diff --git a/src/ui/indicatorwidget.c b/src/ui/indicatorwidget.c

index 96556912..4a829ae3 100644

--- a/src/ui/indicatorwidget.c

+++ b/src/ui/indicatorwidget.c

@@ -72,6 +72,7 @@ void init_IndicatorWidget(iIndicatorWidget *d) {

 iWidget *w = &d->widget;

 init_Widget(w);

 init_Anim(&d->pos, 0);

}

static void startTimer_IndicatorWidget_(iIndicatorWidget *d) {

diff --git a/src/ui/inputwidget.c b/src/ui/inputwidget.c

index 1674040b..600a4462 100644

--- a/src/ui/inputwidget.c

+++ b/src/ui/inputwidget.c

@@ -31,7 +31,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */

#include <SDL_clipboard.h>

#include <SDL_timer.h>

-#if defined (iPlatformApple)

+#if defined (iPlatformAppleDesktop)

include "macos.h"

#endif

@@ -39,7 +39,7 @@ static const int refreshInterval_InputWidget_ = 256;

static const size_t maxUndo_InputWidget_ = 64;

static void enableEditorKeysInMenus_(iBool enable) {

-#if defined (iPlatformApple)

+#if defined (iPlatformAppleDesktop)

 enableMenuItemsByKey_MacOS(SDLK_LEFT, KMOD_PRIMARY, enable);

 enableMenuItemsByKey_MacOS(SDLK_RIGHT, KMOD_PRIMARY, enable);

#else

@@ -81,6 +81,8 @@ struct Impl_InputWidget {

 iArray          text;    /* iChar[] */

 iArray          oldText; /* iChar[] */

 iString         hint;

 size_t          cursor;

 size_t          lastCursor;

 iRanges         mark;

@@ -108,12 +110,14 @@ static void showCursor_InputWidget_(iInputWidget *d) {

void init_InputWidget(iInputWidget *d, size_t maxLen) {

 iWidget *w = &d->widget;

 init_Widget(w);

 init_Array(&d->text, sizeof(iChar));

 init_Array(&d->oldText, sizeof(iChar));

 init_String(&d->hint);

 init_Array(&d->undoStack, sizeof(iInputUndo));

 d->font             = uiInput_FontId | alwaysVariableFlag_FontId;

 d->cursor           = 0;

 d->lastCursor       = 0;

 d->inFlags          = eatEscape_InputWidgetFlag;

@@ -121,6 +125,9 @@ void init_InputWidget(iInputWidget *d, size_t maxLen) {

 setMaxLen_InputWidget(d, maxLen);

 /* Caller must arrange the width, but the height is fixed. */

 w->rect.size.y = lineHeight_Text(default_FontId) + 2 * gap_UI;

+#if defined (iPlatformAppleMobile)

+#endif

 setFlags_Widget(w, fixedHeight_WidgetFlag, iTrue);

 init_Click(&d->click, d, SDL_BUTTON_LEFT);

 d->timer = 0;

@@ -130,6 +137,7 @@ void init_InputWidget(iInputWidget *d, size_t maxLen) {

void deinit_InputWidget(iInputWidget *d) {

 if (isSelected_Widget(d)) {

     enableEditorKeysInMenus_(iTrue);

 }

 delete_TextBuf(d->buffered);

@@ -192,6 +200,16 @@ void setHint_InputWidget(iInputWidget *d, const char *hintText) {

 setCStr_String(&d->hint, hintText);

}

+void setContentPadding_InputWidget(iInputWidget *d, int left, int right) {

+}

static const iChar sensitiveChar_ = 0x25cf; /* black circle */

static iString *visText_InputWidget_(const iInputWidget *d) {

@@ -342,10 +360,13 @@ static void insertChar_InputWidget_(iInputWidget *d, iChar chr) {

         resize_Array(&d->text, d->cursor + 1);

     }

     set_Array(&d->text, d->cursor++, &chr);

         iWidget *nextFocus = findFocusable_Widget(w, forward_WidgetFocusDir);

         setFocus_Widget(nextFocus == w ? NULL : nextFocus);

     }

 }

 showCursor_InputWidget_(d);

 refresh_Widget(as_Widget(d));

@@ -481,7 +502,9 @@ iLocalDef iInt2 padding_(void) {

static iInt2 textOrigin_InputWidget_(const iInputWidget *d, const char *visText) {

 const iWidget *w         = constAs_Widget(d);

 const iInt2    emSize    = advance_Text(d->font, "M");

 const int      textWidth = advance_Text(d->font, visText).x;

 const int      cursorX   = advanceN_Text(d->font, visText, d->cursor).x;

@@ -540,6 +563,27 @@ static iBool copy_InputWidget_(iInputWidget *d, iBool doCut) {

 return iFalse;

}

+static void paste_InputWidget_(iInputWidget *d) {

+}

static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) {

 iWidget *w = as_Widget(d);

 if (isCommand_Widget(w, ev, "focus.gained")) {

@@ -561,7 +605,11 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) {

     return iTrue;

 }

 if (ev->type == SDL_MOUSEMOTION && isHover_Widget(d)) {

 }

 switch (processEvent_Click(&d->click, ev)) {

     case none_ClickResult:

@@ -606,26 +654,7 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) {

                 copy_InputWidget_(d, key == 'x');

                 return iTrue;

             case 'v':

                 return iTrue;

             case 'z':

                 if (popUndo_InputWidget_(d)) {

@@ -637,6 +666,11 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) {

     }

     d->lastCursor = d->cursor;

     switch (key) {

         case SDLK_RETURN:

         case SDLK_KP_ENTER:

             d->inFlags |= enterPressed_InputWidgetFlag;

@@ -664,6 +698,11 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) {

                 remove_Array(&d->text, --d->cursor);

                 contentsWereChanged_InputWidget_(d);

             }

             showCursor_InputWidget_(d);

             refresh_Widget(w);

             return iTrue;

@@ -804,7 +843,7 @@ static void draw_InputWidget_(const iInputWidget *d) {

                         isFocused ? gap_UI / 4 : 1,

                         isFocused ? uiInputFrameFocused_ColorId

                                   : isHover ? uiInputFrameHover_ColorId : uiInputFrame_ColorId);

 const iInt2 textOrigin = textOrigin_InputWidget_(d, cstr_String(text));

 if (isFocused && !isEmpty_Range(&d->mark)) {

     /* Draw the selected range. */

@@ -832,25 +871,37 @@ static void draw_InputWidget_(const iInputWidget *d) {

 /* Cursor blinking. */

 if (isFocused && d->cursorVis) {

     iString cur;

         }

         else {

         }

     }

     else {

     }

     /* The `gap_UI` offsets below are a hack. They are used because for some reason the

        cursor rect and the glyph inside don't quite position like during `run_Text_()`. */

     const iInt2 prefixSize = advanceN_Text(d->font, cstr_String(text), d->cursor);

     fillRect_Paint(&p, curRect, uiInputCursor_ColorId);

 }

 delete_String(text);

 drawChildren_Widget(w);

diff --git a/src/ui/inputwidget.h b/src/ui/inputwidget.h

index 654433ea..3c7b2ffb 100644

--- a/src/ui/inputwidget.h

+++ b/src/ui/inputwidget.h

@@ -38,6 +38,7 @@ void setMaxLen_InputWidget (iInputWidget *, size_t maxLen);

void setText_InputWidget (iInputWidget *, const iString *text);

void setTextCStr_InputWidget (iInputWidget *, const char *cstr);

void setCursor_InputWidget (iInputWidget *, size_t pos);

+void setContentPadding_InputWidget (iInputWidget , int left, int right); / only affects the text entry */

void begin_InputWidget (iInputWidget *);

void end_InputWidget (iInputWidget *, iBool accept);

void selectAll_InputWidget (iInputWidget *);

diff --git a/src/ui/keys.c b/src/ui/keys.c

index 656a52ac..4aa42d36 100644

--- a/src/ui/keys.c

+++ b/src/ui/keys.c

@@ -81,9 +81,13 @@ static const struct { int id; iMenuItem bind; int flags; } defaultBindings_[] =

 { 46, { "Hover on link via home row keys", 'h', 0,                  "document.linkkeys arg:1 hover:1" }, 0 },

 { 47, { "Next set of home row key links", '.', 0,                   "document.linkkeys more:1" }, 0 },

 { 50, { "Add bookmark",              'd', KMOD_PRIMARY,             "bookmark.add"       }, 0 },

 { 70, { "Zoom in",                   SDLK_EQUALS, KMOD_PRIMARY,     "zoom.delta arg:10"  }, 0 },

 { 71, { "Zoom out",                  SDLK_MINUS, KMOD_PRIMARY,      "zoom.delta arg:-10" }, 0 },

 { 72, { "Reset zoom",                SDLK_0, KMOD_PRIMARY,          "zoom.set arg:100"   }, 0 },

+#if !defined (iPlatformApple) /* Ctrl-Cmd-F on macOS */

+#endif

 { 76, { "New tab",                   newTab_KeyShortcut,            "tabs.new"           }, 0 },

 { 77, { "Close tab",                 closeTab_KeyShortcut,          "tabs.close"         }, 0 },

 { 80, { "Previous tab",              prevTab_KeyShortcut,           "tabs.prev"          }, 0 },

diff --git a/src/ui/labelwidget.c b/src/ui/labelwidget.c

index 57136509..8cba2d00 100644

--- a/src/ui/labelwidget.c

+++ b/src/ui/labelwidget.c

@@ -28,8 +28,12 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */

#include "util.h"

#include "keys.h"

-iLocalDef iInt2 padding_(int flags) {

+iLocalDef iInt2 padding_(int64_t flags) {

+#if defined (iPlatformAppleMobile)

+#else

 return init_I2(flags & tight_WidgetFlag ? 3 * gap_UI / 2 : (3 * gap_UI), gap_UI);

+#endif

}

struct Impl_LabelWidget {

@@ -38,6 +42,7 @@ struct Impl_LabelWidget {

 int     font;

 int     key;

 int     kmods;

 iString command;

 iBool   alignVisual; /* align according to visible bounds, not typography */

 iClick  click;

@@ -82,6 +87,19 @@ static iBool processEvent_LabelWidget_(iLabelWidget *d, const SDL_Event *ev) {

     return iFalse;

 }

 if (!isEmpty_String(&d->command)) {

+#if 0 && defined (iPlatformAppleMobile)

+#endif

     switch (processEvent_Click(&d->click, ev)) {

         case started_ClickResult:

             setFlags_Widget(w, pressed_WidgetFlag, iTrue);

@@ -121,16 +139,17 @@ static void keyStr_LabelWidget_(const iLabelWidget *d, iString *str) {

static void getColors_LabelWidget_(const iLabelWidget *d, int *bg, int *fg, int *frame1, int *frame2) {

 const iWidget *w           = constAs_Widget(d);

 const iBool    isButton    = d->click.button != 0;

 /* Default color state. */

 *fg     = uiText_ColorId;

 *frame1 = isButton ? uiEmboss1_ColorId : d->widget.frameColor;

 *frame2 = isButton ? uiEmboss2_ColorId : *frame1;

     *fg = uiTextDisabled_ColorId;

 }

 if (isSel) {

@@ -177,6 +196,9 @@ static void getColors_LabelWidget_(const iLabelWidget *d, int *bg, int *fg, int

     }

     *fg = uiTextPressed_ColorId | permanent_ColorId;

 }

}

static void draw_LabelWidget_(const iLabelWidget *d) {

@@ -205,7 +227,8 @@ static void draw_LabelWidget_(const iLabelWidget *d) {

             bottomRight_Rect(frameRect), bottomLeft_Rect(frameRect)

         };

         drawLines_Paint(&p, points + 2, 3, frame2);

     }

 }

 setClip_Paint(&p, rect);

@@ -256,7 +279,7 @@ static void sizeChanged_LabelWidget_(iLabelWidget *d) {

void updateSize_LabelWidget(iLabelWidget *d) {

 iWidget *w = as_Widget(d);

 iInt2 size = add_I2(measure_Text(d->font, cstr_String(&d->label)), muli_I2(padding_(flags), 2));

 if ((flags & drawKey_WidgetFlag) && d->key) {

     iString str;

@@ -277,6 +300,7 @@ void updateSize_LabelWidget(iLabelWidget *d) {

void init_LabelWidget(iLabelWidget *d, const char *label, const char *cmd) {

 init_Widget(&d->widget);

 d->font = uiLabel_FontId;

 initCStr_String(&d->label, label);

 if (cmd) {

     initCStr_String(&d->command, cmd);

@@ -304,6 +328,13 @@ void setFont_LabelWidget(iLabelWidget *d, int fontId) {

 updateSize_LabelWidget(d);

}

+void setTextColor_LabelWidget(iLabelWidget *d, int color) {

+}

void setText_LabelWidget(iLabelWidget *d, const iString *text) {

 updateText_LabelWidget(d, text);

 updateSize_LabelWidget(d);

diff --git a/src/ui/labelwidget.h b/src/ui/labelwidget.h

index c91793e5..266c3b02 100644

--- a/src/ui/labelwidget.h

+++ b/src/ui/labelwidget.h

@@ -31,6 +31,7 @@ iDeclareObjectConstructionArgs(LabelWidget, const char *label, const char *comma

void setAlignVisually_LabelWidget(iLabelWidget *, iBool alignVisual);

void setFont_LabelWidget (iLabelWidget *, int fontId);

+void setTextColor_LabelWidget (iLabelWidget *, int color);

void setText_LabelWidget (iLabelWidget *, const iString text); / resizes widget */

void setTextCStr_LabelWidget (iLabelWidget *, const char *text);

void setCommand_LabelWidget (iLabelWidget *, const iString *command);

diff --git a/src/ui/listwidget.c b/src/ui/listwidget.c

index b27107df..d31ac2f5 100644

--- a/src/ui/listwidget.c

+++ b/src/ui/listwidget.c

@@ -26,6 +26,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */

#include "util.h"

#include "command.h"

#include "visbuf.h"

+#include "app.h"

#include <the_Foundation/intset.h>

@@ -135,8 +136,13 @@ void updateVisible_ListWidget(iListWidget *d) {

}

void setItemHeight_ListWidget(iListWidget *d, int itemHeight) {

}

int scrollBarWidth_ListWidget(const iListWidget *d) {

@@ -292,12 +298,16 @@ static iBool processEvent_ListWidget_(iListWidget *d, const SDL_Event *ev) {

     setHoverItem_ListWidget_(d, hover);

 }

 if (ev->type == SDL_MOUSEWHEEL && isHover_Widget(w)) {

#if defined (iPlatformApple)

+# if defined (iPlatformAppleDesktop)

+# endif

#else

#endif

     return iTrue;

 }

 switch (processEvent_Click(&d->click, ev)) {

diff --git a/src/ui/lookupwidget.c b/src/ui/lookupwidget.c

index 123ac6c4..7d6052e2 100644

--- a/src/ui/lookupwidget.c

+++ b/src/ui/lookupwidget.c

@@ -377,6 +377,9 @@ void init_LookupWidget(iLookupWidget *d) {

 init_Widget(w);

 setId_Widget(w, "lookup");

 setFlags_Widget(w, focusable_WidgetFlag | resizeChildren_WidgetFlag, iTrue);

+#if defined (iPlatformAppleMobile)

+#endif

 d->list = addChild_Widget(w, iClob(new_ListWidget()));

 setItemHeight_ListWidget(d->list, lineHeight_Text(uiContent_FontId) * 1.25f);

 d->cursor = iInvalidPos;

@@ -626,9 +629,6 @@ static iBool moveCursor_LookupWidget_(iLookupWidget *d, int delta) {

static iBool processEvent_LookupWidget_(iLookupWidget *d, const SDL_Event *ev) {

 iWidget *w = as_Widget(d);

 const char *cmd = command_UserEvent(ev);

-// if (ev->type == SDL_MOUSEMOTION && contains_Widget(w, init_I2(ev->motion.x, ev->motion.y))) {

-// setCursor_Window(get_Window(), SDL_SYSTEM_CURSOR_ARROW);

-// }

 if (isCommand_Widget(w, ev, "lookup.ready")) {

     /* Take the results and present them in the list. */

     presentResults_LookupWidget_(d);

@@ -637,9 +637,23 @@ static iBool processEvent_LookupWidget_(iLookupWidget *d, const SDL_Event *ev) {

 if (isResize_UserEvent(ev) || (equal_Command(cmd, "layout.changed") &&

                                equal_Rangecc(range_Command(cmd, "id"), "navbar"))) {

     /* Position the lookup popup under the URL bar. */ {

         setSize_Widget(w, init_I2(width_Widget(findWidget_App("url")),

         setPos_Widget(w, bottomLeft_Rect(bounds_Widget(findWidget_App("url"))));

+#if defined (iPlatformAppleMobile)

+#endif

         arrange_Widget(w);

     }

     updateVisible_ListWidget(d->list);

diff --git a/src/ui/playerui.c b/src/ui/mediaui.c

similarity index 67%

rename from src/ui/playerui.c

rename to src/ui/mediaui.c

index 3e22a1d2..2fad0cec 100644

--- a/src/ui/playerui.c

+++ b/src/ui/mediaui.c

@@ -20,11 +20,16 @@ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT

(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS

SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */

-#include "playerui.h"

+#include "mediaui.h"

+#include "media.h"

+#include "documentwidget.h"

+#include "gmdocument.h"

#include "audio/player.h"

#include "paint.h"

#include "util.h"

+#include <the_Foundation/path.h>

static const char *volumeChar_(float volume) {

 if (volume <= 0) {

     return "\U0001f507";

@@ -72,8 +77,11 @@ static void drawPlayerButton_(iPaint *p, iRect rect, const char *label, int font

 drawCentered_Text(font, frameRect, iTrue, fg, "%s", label);

}

+static const uint32_t sevenSegmentDigit_ = 0x1fbf0;

+static const char *sevenSegmentStr_ = "\U0001fbf0";

static int drawSevenSegmentTime_(iInt2 pos, int color, int align, int seconds) { /* returns width */

 const int hours = seconds / 3600;

 const int mins  = (seconds / 60) % 60;

 const int secs  = seconds % 60;

@@ -81,14 +89,14 @@ static int drawSevenSegmentTime_(iInt2 pos, int color, int align, int seconds) {

 iString   num;

 init_String(&num);

 if (hours) {

     appendChar_String(&num, ':');

 }

 appendChar_String(&num, ':');

 iInt2 size = advanceRange_Text(font, range_String(&num));

 if (align == right_Alignment) {

     pos.x -= size.x;

@@ -134,10 +142,10 @@ void draw_PlayerUI(iPlayerUI *d, iPaint *p) {

                               iRound(totalTime));

 }

 /* Scrubber. */

 const int   scrubMax = (s2 - s1) * streamProgress_Player(d->player);

 drawHLine_Paint(p, init_I2(s1, yMid), part, bright);

 drawHLine_Paint(p, init_I2(s1 + part, yMid), scrubMax - part, dim);

@@ -182,3 +190,84 @@ void draw_PlayerUI(iPlayerUI *d, iPaint *p) {

               dot);

 }

}

+/----------------------------------------------------------------------------------------------/

+static void drawSevenSegmentBytes_(iInt2 pos, int color, size_t numBytes) {

+}

+void init_DownloadUI(iDownloadUI *d, const iDocumentWidget *doc, uint16_t mediaId, iRect bounds) {

+}

+iBool processEvent_DownloadUI(iDownloadUI *d, const SDL_Event *ev) {

+}

+void draw_DownloadUI(const iDownloadUI *d, iPaint *p) {

+}

diff --git a/src/ui/playerui.h b/src/ui/mediaui.h

similarity index 76%

rename from src/ui/playerui.h

rename to src/ui/mediaui.h

index a1f4ca9b..e79dedc0 100644

--- a/src/ui/playerui.h

+++ b/src/ui/mediaui.h

@@ -23,6 +23,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */

#pragma once

#include <the_Foundation/rect.h>

+#include <SDL_events.h>

iDeclareType(Paint)

iDeclareType(Player)

@@ -42,3 +43,19 @@ struct Impl_PlayerUI {

void init_PlayerUI (iPlayerUI *, const iPlayer *player, iRect bounds);

void draw_PlayerUI (iPlayerUI *, iPaint *p);

+/----------------------------------------------------------------------------------------------/

+iDeclareType(DocumentWidget)

+iDeclareType(Media)

+iDeclareType(DownloadUI)

+struct Impl_DownloadUI {

+};

+void init_DownloadUI (iDownloadUI *, const iDocumentWidget *doc, uint16_t mediaId, iRect bounds);

+iBool processEvent_DownloadUI (iDownloadUI *, const SDL_Event *ev);

+void draw_DownloadUI (const iDownloadUI *, iPaint *p);

diff --git a/src/ui/scrollwidget.c b/src/ui/scrollwidget.c

index 649d0575..448103b5 100644

--- a/src/ui/scrollwidget.c

+++ b/src/ui/scrollwidget.c

@@ -41,7 +41,7 @@ void init_ScrollWidget(iScrollWidget *d) {

 setId_Widget(w, "scroll");

 setFlags_Widget(w,

                 fixedWidth_WidgetFlag | resizeToParentHeight_WidgetFlag |

                 iTrue);

 w->rect.size.x = gap_UI * 3;

 init_Click(&d->click, d, SDL_BUTTON_LEFT);

@@ -137,7 +137,7 @@ static void draw_ScrollWidget_(const iScrollWidget *d) {

 if (bounds.size.x > 0) {

     iPaint p;

     init_Paint(&p);

     const iRect thumbRect = shrunk_Rect(

         thumbRect_ScrollWidget_(d), init_I2(isPressed ? gap_UI : (gap_UI * 4 / 3), gap_UI / 2));

     fillRect_Paint(&p, thumbRect, isPressed ? uiBackgroundPressed_ColorId : tmQuote_ColorId);

diff --git a/src/ui/sidebarwidget.c b/src/ui/sidebarwidget.c

index 679d8e6f..4e908f1e 100644

--- a/src/ui/sidebarwidget.c

+++ b/src/ui/sidebarwidget.c

@@ -96,6 +96,7 @@ struct Impl_SidebarWidget {

 iLabelWidget *    modeButtons[max_SidebarMode];

 int               maxButtonLabelWidth;

 int               width;

 iWidget *         resizer;

 iWidget *         menu;

 iSidebarItem *    contextItem; /* list item accessed in the context menu */

@@ -148,6 +149,10 @@ static void updateItems_SidebarWidget_(iSidebarWidget *d) {

             if (isHidden_FeedEntry(entry)) {

                 continue; /* A hidden entry. */

             }

             /* Exclude entries that are too old for Visited to keep track of. */

             if (secondsSince_Time(&now, &entry->discovered) > maxAge_Visited) {

                 break; /* the rest are even older */

@@ -191,10 +196,10 @@ static void updateItems_SidebarWidget_(iSidebarWidget *d) {

         d->menu = makeMenu_Widget(

             as_Widget(d),

             (iMenuItem[]){ { "Open Entry in New Tab", 0, 0, "feed.entry.opentab" },

                            { "Mark as Read", 0, 0, "feed.entry.toggleread" },

                            { "Add Bookmark...", 0, 0, "feed.entry.bookmark" },

                            { "---", 0, 0, NULL },

                            { "Edit Feed...", 0, 0, "feed.entry.edit" },

                            { uiTextCaution_ColorEscape "Unsubscribe...", 0, 0, "feed.entry.unsubscribe" },

                            { "---", 0, 0, NULL },

@@ -211,6 +216,7 @@ static void updateItems_SidebarWidget_(iSidebarWidget *d) {

             item->id = index_ArrayConstIterator(&i);

             setRange_String(&item->label, head->text);

             item->indent = head->level * 5 * gap_UI;

             addItem_ListWidget(d->list, item);

             iRelease(item);

         }

@@ -322,7 +328,7 @@ static void updateItems_SidebarWidget_(iSidebarWidget *d) {

         iConstForEach(PtrArray, i, identities_GmCerts(certs_App())) {

             const iGmIdentity *ident = i.ptr;

             iSidebarItem *item = new_SidebarItem();

             item->icon = ident->icon;

             set_String(&item->label, collect_String(subject_TlsCertificate(ident->cert)));

             iDate until;

@@ -406,6 +412,11 @@ static void updateItems_SidebarWidget_(iSidebarWidget *d) {

 }

}

+static void updateItemHeight_SidebarWidget_(iSidebarWidget *d) {

+}

iBool setMode_SidebarWidget(iSidebarWidget *d, enum iSidebarMode mode) {

 if (d->mode == mode) {

     return iFalse;

@@ -417,22 +428,21 @@ iBool setMode_SidebarWidget(iSidebarWidget *d, enum iSidebarMode mode) {

 for (enum iSidebarMode i = 0; i < max_SidebarMode; i++) {

     setFlags_Widget(as_Widget(d->modeButtons[i]), selected_WidgetFlag, i == d->mode);

 }

 setBackgroundColor_Widget(as_Widget(d->list),

                           d->mode == documentOutline_SidebarMode ? tmBannerBackground_ColorId

 /* Restore previous scroll position. */

 setScrollPos_ListWidget(d->list, d->modeScroll[mode]);

 return iTrue;

}

enum iSidebarMode mode_SidebarWidget(const iSidebarWidget *d) {

}

int width_SidebarWidget(const iSidebarWidget *d) {

}

static const char *normalModeLabels_[max_SidebarMode] = {

@@ -451,6 +461,10 @@ static const char *tightModeLabels_[max_SidebarMode] = {

 "\U0001f5b9",

};

+const char *icon_SidebarMode(enum iSidebarMode mode) {

+}

void init_SidebarWidget(iSidebarWidget *d, enum iSidebarSide side) {

 iWidget *w = as_Widget(d);

 init_Widget(w);

@@ -464,38 +478,66 @@ void init_SidebarWidget(iSidebarWidget *d, enum iSidebarSide side) {

                 iTrue);

 iZap(d->modeScroll);

 d->side = side;

+#if defined (iPlatformAppleMobile)

+#else

 d->width = 60 * gap_UI;

+#endif

 setFlags_Widget(w, fixedWidth_WidgetFlag, iTrue);

 iWidget *vdiv = makeVDiv_Widget();

 addChildFlags_Widget(w, vdiv, resizeToParentWidth_WidgetFlag | resizeToParentHeight_WidgetFlag);

+// if (deviceType_App() == phone_AppDeviceType) {

 }

 iWidget *content = new_Widget();

 setFlags_Widget(content, resizeChildren_WidgetFlag, iTrue);

 d->list = new_ListWidget();

 setPadding_Widget(as_Widget(d->list), 0, gap_UI, 0, gap_UI);

 d->contextItem = NULL;

 d->blank = new_Widget();

 addChildFlags_Widget(content, iClob(d->blank), resizeChildren_WidgetFlag);

 addChildFlags_Widget(vdiv, iClob(content), expand_WidgetFlag);

 d->resizer =

     addChildFlags_Widget(w,

                          iClob(new_Widget()),

@@ -503,6 +545,9 @@ void init_SidebarWidget(iSidebarWidget *d, enum iSidebarSide side) {

                              resizeToParentHeight_WidgetFlag |

                              (side == left_SideBarSide ? moveToParentRightEdge_WidgetFlag

                                                        : moveToParentLeftEdge_WidgetFlag));

 setId_Widget(d->resizer, side == left_SideBarSide ? "sidebar.grab" : "sidebar2.grab");

 d->resizer->rect.size.x = gap_UI;

 setBackgroundColor_Widget(d->resizer, none_ColorId);

@@ -514,6 +559,18 @@ void deinit_SidebarWidget(iSidebarWidget *d) {

 deinit_String(&d->cmdPrefix);

}

+void setButtonFont_SidebarWidget(iSidebarWidget *d, int font) {

+}

static const iGmIdentity *constHoverIdentity_SidebarWidget_(const iSidebarWidget *d) {

 if (d->mode == identities_SidebarMode) {

     const iSidebarItem *hoverItem = constHoverItem_ListWidget(d->list);

@@ -581,9 +638,11 @@ static void itemClicked_SidebarWidget_(iSidebarWidget *d, const iSidebarItem *it

}

static void checkModeButtonLayout_SidebarWidget_(iSidebarWidget *d) {

 const iBool isTight =

     (width_Rect(bounds_Widget(as_Widget(d->modeButtons[0]))) < d->maxButtonLabelWidth);

 for (int i = 0; i < max_SidebarMode; i++) {

     if (isTight && ~flags_Widget(as_Widget(d->modeButtons[i])) & tight_WidgetFlag) {

         setFlags_Widget(as_Widget(d->modeButtons[i]), tight_WidgetFlag, iTrue);

         updateTextCStr_LabelWidget(d->modeButtons[i], tightModeLabels_[i]);

@@ -596,17 +655,20 @@ static void checkModeButtonLayout_SidebarWidget_(iSidebarWidget *d) {

}

void setWidth_SidebarWidget(iSidebarWidget *d, int width) {

 d->width = width;

 if (isVisible_Widget(w)) {

     w->rect.size.x = width;

 }

 checkModeButtonLayout_SidebarWidget_(d);

 if (!isRefreshPending_App()) {

     updateSize_DocumentWidget(document_App());

     invalidate_ListWidget(d->list);

@@ -621,12 +683,24 @@ iBool handleBookmarkEditorCommands_SidebarWidget_(iWidget *editor, const char *c

         const iString *title = text_InputWidget(findChild_Widget(editor, "bmed.title"));

         const iString *url   = text_InputWidget(findChild_Widget(editor, "bmed.url"));

         const iString *tags  = text_InputWidget(findChild_Widget(editor, "bmed.tags"));

         const iSidebarItem *item = hoverItem_ListWidget(d->list);

         iAssert(item); /* hover item cannot have been changed */

         iBookmark *bm = get_Bookmarks(bookmarks_App(), item->id);

         set_String(&bm->title, title);

         set_String(&bm->url, url);

         set_String(&bm->tags, tags);

         postCommand_App("bookmarks.changed");

     }

     setFlags_Widget(as_Widget(d), disabled_WidgetFlag, iFalse);

@@ -650,6 +724,9 @@ static iBool handleSidebarCommand_SidebarWidget_(iSidebarWidget *d, const char *

         postCommandf_App("%s.toggle", cstr_String(id_Widget(w)));

     }

     scrollOffset_ListWidget(d->list, 0);

     return iTrue;

 }

 else if (equal_Command(cmd, "toggle")) {

@@ -771,6 +848,10 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev)

             setText_InputWidget(findChild_Widget(dlg, "bmed.title"), &bm->title);

             setText_InputWidget(findChild_Widget(dlg, "bmed.url"), &bm->url);

             setText_InputWidget(findChild_Widget(dlg, "bmed.tags"), &bm->tags);

             setCommandHandler_Widget(dlg, handleBookmarkEditorCommands_SidebarWidget_);

             setFocus_Widget(findChild_Widget(dlg, "bmed.title"));

         }

@@ -878,11 +959,12 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev)

                             uiTextCaution_ColorEscape "UNSUBSCRIBE",

                             format_CStr("Really unsubscribe from feed\n\"%s\"?",

                                         cstr_String(&feedBookmark->title)),

                             2);

                     }

                     return iTrue;

@@ -967,9 +1049,9 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev)

                     "%s\n" uiText_ColorEscape

                     "including its certificate and private key files?",

                     cstr_String(&item->label)),

                 2);

             return iTrue;

         }

@@ -1004,12 +1086,12 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev)

     }

     else if (equal_Command(cmd, "history.clear")) {

         if (argLabel_Command(cmd, "confirm")) {

         }

         else {

             clear_Visited(visited_App());

@@ -1166,8 +1248,8 @@ static void draw_SidebarItem_(const iSidebarItem *d, iPaint *p, iRect itemRect,

 const int itemHeight     = height_Rect(itemRect);

 const int iconColor      = isHover ? (isPressing ? uiTextPressed_ColorId : uiIconHover_ColorId)

                                    : uiIcon_ColorId;

 if (isHover) {

     bg = isPressing ? uiBackgroundPressed_ColorId

                     : uiBackgroundFramelessHover_ColorId;

@@ -1197,6 +1279,7 @@ static void draw_SidebarItem_(const iSidebarItem *d, iPaint *p, iRect itemRect,

 else if (sidebar->mode == feeds_SidebarMode) {

     const int fg = isHover ? (isPressing ? uiTextPressed_ColorId : uiTextFramelessHover_ColorId)

                            : uiText_ColorId;

     if (d->listItem.isSeparator) {

         if (d != constItem_ListWidget(list, 0)) {

             drawHLine_Paint(p,

@@ -1205,19 +1288,18 @@ static void draw_SidebarItem_(const iSidebarItem *d, iPaint *p, iRect itemRect,

                             uiSeparator_ColorId);

         }

         drawRange_Text(

             add_I2(pos,

                    init_I2(3 * gap_UI,

             uiIcon_ColorId,

             range_String(&d->meta));

     }

     else {

         const iBool isUnread = (d->indent != 0);

         const int h1 = lineHeight_Text(uiLabel_FontId);

         const int h2 = lineHeight_Text(titleFont);

         iRect iconArea = { addY_I2(pos, 0), init_I2(iconPad, itemHeight) };

         if (isUnread) {

             fillRect_Paint(

@@ -1229,12 +1311,14 @@ static void draw_SidebarItem_(const iSidebarItem *d, iPaint *p, iRect itemRect,

             /* TODO: Use the primary hue from the theme of this site. */

             iString str;

             initUnicodeN_String(&str, &d->icon, 1);

                               adjusted_Rect(iconArea, init_I2(gap_UI, 0), zero_I2()),

                               iTrue,

                               isHover && isPressing

                                   ? iconColor

                               "%s",

                               cstr_String(&str));

             deinit_String(&str);

@@ -1278,7 +1362,8 @@ static void draw_SidebarItem_(const iSidebarItem *d, iPaint *p, iRect itemRect,

     iString str;

     init_String(&str);

     appendChar_String(&str, d->icon ? d->icon : 0x1f588);

     drawCentered_Text(font,

                       iconArea,

                       iTrue,

@@ -1315,9 +1400,9 @@ static void draw_SidebarItem_(const iSidebarItem *d, iPaint *p, iRect itemRect,

                             width_Rect(itemRect) - scrollBarWidth,

                             uiSeparator_ColorId);

             drawRange_Text(

                 add_I2(drawPos,

                 uiIcon_ColorId,

                 range_String(&d->meta));

         }

diff --git a/src/ui/sidebarwidget.h b/src/ui/sidebarwidget.h

index fa74e049..d7029b0f 100644

--- a/src/ui/sidebarwidget.h

+++ b/src/ui/sidebarwidget.h

@@ -33,6 +33,8 @@ enum iSidebarMode {

 max_SidebarMode

};

+const char * icon_SidebarMode (enum iSidebarMode mode);

enum iSidebarSide {

 left_SideBarSide,

 right_SideBarSide,

@@ -41,8 +43,9 @@ enum iSidebarSide {

iDeclareWidgetClass(SidebarWidget)

iDeclareObjectConstructionArgs(SidebarWidget, enum iSidebarSide side)

-iBool setMode_SidebarWidget (iSidebarWidget *, enum iSidebarMode mode);

+iBool setMode_SidebarWidget (iSidebarWidget *, enum iSidebarMode mode);

+void setButtonFont_SidebarWidget (iSidebarWidget *, int font);

-enum iSidebarMode mode_SidebarWidget (const iSidebarWidget *);

-int width_SidebarWidget (const iSidebarWidget *);

-void setWidth_SidebarWidget (iSidebarWidget *, int width);

+enum iSidebarMode mode_SidebarWidget (const iSidebarWidget *);

+int width_SidebarWidget (const iSidebarWidget *);

+void setWidth_SidebarWidget (iSidebarWidget *, int width);

diff --git a/src/ui/text.c b/src/ui/text.c

index 93ea323e..ee838ce7 100644

--- a/src/ui/text.c

+++ b/src/ui/text.c

@@ -234,6 +234,18 @@ static void initFonts_Text_(iText *d) {

     italicFont  = &fontLiterataLightItalicopsz10_Embedded;

     lightFont   = &fontLiterataExtraLightopsz18_Embedded;

 }

 if (d->headingFont == firaSans_TextFont) {

     h12Font     = &fontFiraSansBold_Embedded;

     h3Font      = &fontFiraSansRegular_Embedded;

@@ -248,18 +260,34 @@ static void initFonts_Text_(iText *d) {

     h12Font = &fontLiterataBoldopsz36_Embedded;

     h3Font  = &fontLiterataRegularopsz14_Embedded;

 }

+#if defined (iPlatformAppleMobile)

+#else

+#endif

 const struct {

     const iBlock *ttf;

     int size;

     float scaling;

     int symbolsFont;

 } fontData[max_FontId] = {

     { &fontSourceSansProRegular_Embedded, textSize,             scaling, symbols_FontId },

     /* content fonts */

     { regularFont,                        textSize,             scaling,      symbols_FontId },

@@ -274,9 +302,10 @@ static void initFonts_Text_(iText *d) {

     /* monospace content fonts */

     { &fontIosevkaTermExtended_Embedded,  textSize,             0.866f, symbols_FontId },

     /* symbol fonts */

     { &fontSymbola_Embedded,              textSize,             1.0f, symbols_FontId },

     { &fontSymbola_Embedded,              textSize * 1.200f,    1.0f, mediumSymbols_FontId },

     { &fontSymbola_Embedded,              textSize * 1.333f,    1.0f, bigSymbols_FontId },

@@ -285,9 +314,10 @@ static void initFonts_Text_(iText *d) {

     { &fontSymbola_Embedded,              monoSize,             1.0f, monospaceSymbols_FontId },

     { &fontSymbola_Embedded,              smallMonoSize,        1.0f, monospaceSmallSymbols_FontId },

     /* emoji fonts */

     { &fontNotoEmojiRegular_Embedded,     textSize,             1.0f, symbols_FontId },

     { &fontNotoEmojiRegular_Embedded,     textSize * 1.200f,    1.0f, mediumSymbols_FontId },

     { &fontNotoEmojiRegular_Embedded,     textSize * 1.333f,    1.0f, bigSymbols_FontId },

@@ -296,7 +326,7 @@ static void initFonts_Text_(iText *d) {

     { &fontNotoEmojiRegular_Embedded,     monoSize,             1.0f, monospaceSymbols_FontId },

     { &fontNotoEmojiRegular_Embedded,     smallMonoSize,        1.0f, monospaceSmallSymbols_FontId },

     /* japanese fonts */

     { &fontNotoSansJPRegular_Embedded,    smallMonoSize,        1.0f, monospaceSmallSymbols_FontId },

     { &fontNotoSansJPRegular_Embedded,    monoSize,             1.0f, monospaceSymbols_FontId },

     { &fontNotoSansJPRegular_Embedded,    textSize,             1.0f, symbols_FontId },

@@ -305,7 +335,7 @@ static void initFonts_Text_(iText *d) {

     { &fontNotoSansJPRegular_Embedded,    textSize * 1.666f,    1.0f, largeSymbols_FontId },

     { &fontNotoSansJPRegular_Embedded,    textSize * 2.000f,    1.0f, hugeSymbols_FontId },

     /* korean fonts */

     { &fontNanumGothicRegular_Embedded,   smallMonoSize,        1.0f, monospaceSmallSymbols_FontId },

     { &fontNanumGothicRegular_Embedded,   monoSize,             1.0f, monospaceSymbols_FontId },

     { &fontNanumGothicRegular_Embedded,   textSize,             1.0f, symbols_FontId },

@@ -331,6 +361,7 @@ static void initFonts_Text_(iText *d) {

        the other sizes. */

     font_Text_(default_FontId)->japaneseFont          = defaultJapanese_FontId;

     font_Text_(defaultMedium_FontId)->japaneseFont    = defaultJapanese_FontId;

     font_Text_(defaultLarge_FontId)->japaneseFont     = defaultJapanese_FontId;

     font_Text_(defaultMonospace_FontId)->japaneseFont = defaultJapanese_FontId;

     font_Text_(monospaceSmall_FontId)->japaneseFont   = monospaceSmallJapanese_FontId;

@@ -344,6 +375,7 @@ static void initFonts_Text_(iText *d) {

 /* Korean script. */ {

     font_Text_(default_FontId)->koreanFont          = defaultKorean_FontId;

     font_Text_(defaultMedium_FontId)->koreanFont    = defaultKorean_FontId;

     font_Text_(defaultLarge_FontId)->koreanFont     = defaultKorean_FontId;

     font_Text_(defaultMonospace_FontId)->koreanFont = defaultKorean_FontId;

     font_Text_(monospaceSmall_FontId)->koreanFont   = monospaceSmallKorean_FontId;

@@ -386,6 +418,7 @@ static void initCache_Text_(iText *d) {

     pushBack_Array(&d->cacheRows, &(iCacheRow){ .height = 0 });

 }

 d->cacheBottom = 0;

 d->cache = SDL_CreateTexture(d->render,

                              SDL_PIXELFORMAT_RGBA4444,

                              SDL_TEXTUREACCESS_STATIC | SDL_TEXTUREACCESS_TARGET,

@@ -669,7 +702,7 @@ static iChar nextChar_(const char **chPos, const char *end) {

}

static enum iFontId fontId_Text_(const iFont *font) {

}

iLocalDef iBool isWrapBoundary_(iChar prevC, iChar c) {

@@ -706,6 +739,7 @@ struct Impl_RunArgs {

 iInt2         pos;

 int           xposLimit;       /* hard limit for wrapping */

 int           xposLayoutBound; /* visible bound for layout purposes; does not affect wrapping */

 const char ** continueFrom_out;

 int *         runAdvance_out;

};

@@ -735,8 +769,7 @@ static iRect run_Font_(iFont *d, const iRunArgs *args) {

 for (const char *chPos = args->text.start; chPos != args->text.end; ) {

     iAssert(chPos < args->text.end);

     const char *currentPos = chPos;

         chPos++;

         iRegExpMatch m;

         init_RegExpMatch(&m);

@@ -761,17 +794,6 @@ static iRect run_Font_(iFont *d, const iRunArgs *args) {

             ch = nextChar_(&chPos, args->text.end);

         }

     }

-#if 0

-#endif

     if (isVariationSelector_Char(ch)) {

         ch = nextChar_(&chPos, args->text.end); /* skip it */

     }

@@ -822,9 +844,17 @@ static iRect run_Font_(iFont *d, const iRunArgs *args) {

             continue;

         }

         if (ch == '\r') {

             if (mode & draw_RunMode && ~mode & permanentColorFlag_RunMode) {

                 SDL_SetTextureColorMod(text_.cache, clr.r, clr.g, clr.b);

             }

             prevCh = 0;

@@ -1024,9 +1054,10 @@ static void drawBounded_Text_(int fontId, iInt2 pos, int xposBound, int color, i

           &(iRunArgs){ .mode = draw_RunMode |

                                (color & permanent_ColorId ? permanentColorFlag_RunMode : 0) |

                                runFlagsFromId_(fontId),

}

static void draw_Text_(int fontId, iInt2 pos, int color, iRangecc text) {

@@ -1112,6 +1143,7 @@ void drawCentered_Text(int fontId, iRect rect, iBool alignVisual, int color, con

 iRect          textBounds = alignVisual ? visualBounds_Text(fontId, text)

                                : (iRect){ zero_I2(), advanceRange_Text(fontId, text) };

 textBounds.pos = sub_I2(mid_Rect(rect), mid_Rect(textBounds));

 draw_Text_(fontId, textBounds.pos, color, text);

 deinit_Block(&chars);

}

@@ -1232,6 +1264,7 @@ iDefineTypeConstructionArgs(TextBuf, (int font, const char *text), font, text)

void init_TextBuf(iTextBuf *d, int font, const char *text) {

 SDL_Renderer *render = text_.render;

 d->size    = advance_Text(font, text);

 d->texture = SDL_CreateTexture(render,

                                SDL_PIXELFORMAT_RGBA4444,

                                SDL_TEXTUREACCESS_STATIC | SDL_TEXTUREACCESS_TARGET,

diff --git a/src/ui/text.h b/src/ui/text.h

index 217bdd13..f696b2e3 100644

--- a/src/ui/text.h

+++ b/src/ui/text.h

@@ -34,7 +34,10 @@ enum iFontId {

 defaultBold_FontId,

 defaultMedium_FontId,

 defaultMediumBold_FontId,

 defaultLarge_FontId,

 defaultMonospace_FontId,

 defaultContentSized_FontId,    

 /* content fonts */

@@ -52,6 +55,7 @@ enum iFontId {

 /* symbol fonts */

 defaultSymbols_FontId,

 defaultMediumSymbols_FontId,

 defaultLargeSymbols_FontId,

 symbols_FontId,

 mediumSymbols_FontId,

@@ -63,6 +67,7 @@ enum iFontId {

 /* emoji fonts */

 defaultEmoji_FontId,

 defaultMediumEmoji_FontId,

 defaultLargeEmoji_FontId,

 emoji_FontId,

 mediumEmoji_FontId,

@@ -92,7 +97,7 @@ enum iFontId {

 max_FontId,

 /* Meta: */

 mask_FontId                     = 0xffff,

 alwaysVariableFlag_FontId       = 0x10000,

@@ -100,8 +105,9 @@ enum iFontId {

 uiLabel_FontId          = default_FontId,

 uiLabelBold_FontId      = defaultBold_FontId,

 uiLabelLarge_FontId     = defaultLarge_FontId,

 uiShortcuts_FontId      = default_FontId,

 uiContent_FontId        = defaultMedium_FontId,

 uiContentBold_FontId    = defaultMediumBold_FontId,

 uiContentSymbols_FontId = defaultMediumSymbols_FontId,

@@ -142,6 +148,8 @@ enum iTextFont {

 firaSans_TextFont,

 literata_TextFont,

 tinos_TextFont,

};

extern int gap_Text; /* affected by content font size */

diff --git a/src/ui/touch.c b/src/ui/touch.c

new file mode 100644

index 00000000..5bf1b87e

--- /dev/null

+++ b/src/ui/touch.c

@@ -0,0 +1,413 @@

+/* Copyright 2021 Jaakko Keränen jaakko.keranen@iki.fi

+Redistribution and use in source and binary forms, with or without

+modification, are permitted provided that the following conditions are met:

+1. Redistributions of source code must retain the above copyright notice, this

+2. Redistributions in binary form must reproduce the above copyright notice,

+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND

+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED

+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE

+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR

+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES

+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;

+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON

+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT

+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS

+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */

+#include "touch.h"

+#include "window.h"

+#include "app.h"

+#include <the_Foundation/array.h>

+#include <the_Foundation/math.h>

+#include <SDL_timer.h>

+iDeclareType(Touch)

+iDeclareType(TouchState)

+iDeclareType(Momentum)

+#define numHistory_Touch_ 3

+#define lastIndex_Touch_ (numHistory_Touch_ - 1)

+enum iTouchEdge {

+};

+struct Impl_Touch {

+};

+iLocalDef void pushPos_Touch_(iTouch *d, const iFloat3 pos, uint32_t time) {

+}

+struct Impl_Momentum {

+};

+struct Impl_TouchState {

+};

+static iTouchState *touchState_(void) {

+}

+static iTouch *find_TouchState_(iTouchState *d, SDL_FingerID id) {

+}

+static const uint32_t longPressSpanMs_ = 425;

+static const int tapRadiusPt_ = 15;

+static iBool isStationary_Touch_(const iTouch *d) {

+}

+static void dispatchMotion_Touch_(iFloat3 pos, int buttonState) {

+}

+static void dispatchClick_Touch_(const iTouch *d, int button) {

+}

+static void clearWidgetMomentum_TouchState_(iTouchState *d, iWidget *widget) {

+}

+static void update_TouchState_(void *ptr) {

+// printf("mom steps:%d\n", numSteps);

+}

+void widgetDestroyed_Touch(iWidget *widget) {

+}

+static void dispatchButtonUp_Touch_(iFloat3 pos) {

+}

+iBool processEvent_Touch(const SDL_Event *ev) {

+// printf("aff:%p (%s)\n", aff, aff ? class_Widget(aff)->name : "-");

+// printf("%p (%s) py: %i wy: %f acc: %f\n",

+// touch->affinity,

+// class_Widget(touch->affinity)->name,

+// pixels.y, y_F3(amount), y_F3(touch->accum));

+// printf("elap:%ums vel:%f\n", elapsed, length_F3(velocity));

+}

diff --git a/src/ui/touch.h b/src/ui/touch.h

new file mode 100644

index 00000000..39a059da

--- /dev/null

+++ b/src/ui/touch.h

@@ -0,0 +1,32 @@

+/* Copyright 2021 Jaakko Keränen jaakko.keranen@iki.fi

+Redistribution and use in source and binary forms, with or without

+modification, are permitted provided that the following conditions are met:

+1. Redistributions of source code must retain the above copyright notice, this

+2. Redistributions in binary form must reproduce the above copyright notice,

+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND

+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED

+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE

+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR

+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES

+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;

+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON

+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT

+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS

+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */

+#pragma once

+#include <the_Foundation/defs.h>

+#include <SDL_events.h>

+iDeclareType(Widget)

+iBool processEvent_Touch (const SDL_Event *);

+void update_Touch (void);

+void widgetDestroyed_Touch (iWidget *widget);

diff --git a/src/ui/util.c b/src/ui/util.c

index d64a93b6..3e57c2b5 100644

--- a/src/ui/util.c

+++ b/src/ui/util.c

@@ -37,6 +37,10 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */

#include "text.h"

#include "window.h"

+#if defined (iPlatformAppleMobile)

+# include "../ios.h"

+#endif

#include <the_Foundation/math.h>

#include <the_Foundation/path.h>

#include <SDL_timer.h>

@@ -194,14 +198,18 @@ static float valueAt_Anim_(const iAnim *d, const uint32_t now) {

     return d->from;

 }

 float t = pos_Anim_(d, now);

 if ((d->flags & easeBoth_AnimFlag) == easeBoth_AnimFlag) {

     t = easeBoth_(t);

 }

 else if (d->flags & easeIn_AnimFlag) {

     t = easeIn_(t);

 }

 else if (d->flags & easeOut_AnimFlag) {

     t = easeOut_(t);

 }

 return d->from * (1.0f - t) + d->to * t;

}

@@ -354,13 +362,13 @@ iLabelWidget *makeHeading_Widget(const char *text) {

iWidget *makeVDiv_Widget(void) {

 iWidget *div = new_Widget();

 return div;

}

iWidget *makeHDiv_Widget(void) {

 iWidget *div = new_Widget();

 return div;

}

@@ -374,11 +382,21 @@ iWidget *addAction_Widget(iWidget *parent, int key, int kmods, const char *comma

/-----------------------------------------------------------------------------------------------/

static iBool isCommandIgnoredByMenus_(const char *cmd) {

        startsWith_CStr(cmd, "feeds.update.") ||

        equal_Command(cmd, "bookmarks.request.finished") ||

        equal_Command(cmd, "window.reload.update") ||

        equal_Command(cmd, "window.mouse.exited") ||

        equal_Command(cmd, "window.mouse.entered") ||

@@ -420,10 +438,24 @@ static iBool menuHandler_(iWidget *menu, const char *cmd) {

 return iFalse;

}

+static iWidget *makeMenuSeparator_(void) {

+}

iWidget *makeMenu_Widget(iWidget *parent, const iMenuItem *items, size_t n) {

 iWidget *menu = new_Widget();

 setFrameColor_Widget(menu, uiSeparator_ColorId);

 setBackgroundColor_Widget(menu, uiBackground_ColorId);

 setFlags_Widget(menu,

                 keepOnTop_WidgetFlag | collapse_WidgetFlag | hidden_WidgetFlag |

                     arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag |

@@ -432,10 +464,7 @@ iWidget *makeMenu_Widget(iWidget *parent, const iMenuItem *items, size_t n) {

 for (size_t i = 0; i < n; ++i) {

     const iMenuItem *item = &items[i];

     if (equal_CStr(item->label, "---")) {

     }

     else {

         iLabelWidget *label = addChildFlags_Widget(

@@ -443,8 +472,21 @@ iWidget *makeMenu_Widget(iWidget *parent, const iMenuItem *items, size_t n) {

             iClob(newKeyMods_LabelWidget(item->label, item->key, item->kmods, item->command)),

             frameless_WidgetFlag | alignLeft_WidgetFlag | drawKey_WidgetFlag);

         updateSize_LabelWidget(label); /* drawKey was set */

     }

 }

 addChild_Widget(parent, iClob(menu));

 setCommandHandler_Widget(menu, menuHandler_);

 iWidget *cancel = addAction_Widget(menu, SDLK_ESCAPE, 0, "cancel");

@@ -454,21 +496,48 @@ iWidget *makeMenu_Widget(iWidget *parent, const iMenuItem *items, size_t n) {

}

void openMenu_Widget(iWidget *d, iInt2 coord) {

 /* Menu closes when commands are emitted, so handle any pending ones beforehand. */

 postCommand_App("cancel"); /* dismiss any other menus */

 processEvents_App(postedEventsOnly_AppEventMode);

 setFlags_Widget(d, hidden_WidgetFlag, iFalse);

 setFlags_Widget(d, commandOnMouseMiss_WidgetFlag, iTrue);

 setFlags_Widget(findChild_Widget(d, "menu.cancel"), disabled_WidgetFlag, iFalse);

 arrange_Widget(d);

 /* Ensure the full menu is visible. */

 const iRect bounds       = bounds_Widget(d);

+#if defined (iPlatformAppleMobile)

+#endif

 if (bottomExcess > 0) {

     d->rect.pos.y -= bottomExcess;

 }

@@ -483,6 +552,10 @@ void openMenu_Widget(iWidget *d, iInt2 coord) {

 }

 postRefresh_App();

 postCommand_Widget(d, "menu.opened");

}

void closeMenu_Widget(iWidget *d) {

@@ -600,7 +673,8 @@ static void addTabPage_Widget_(iWidget *tabs, enum iWidgetAddPos addPos, iWidget

     addPos);

 setFlags_Widget(buttons, hidden_WidgetFlag, iFalse);

 setFlags_Widget(button, selected_WidgetFlag, isSel);

 addChildPos_Widget(pages, page, addPos);

 setFlags_Widget(page, hidden_WidgetFlag | disabled_WidgetFlag, !isSel);

}

@@ -764,7 +838,8 @@ iWidget *makeSheet_Widget(const char *id) {

 setFrameColor_Widget(sheet, uiSeparator_ColorId);

 setBackgroundColor_Widget(sheet, uiBackground_ColorId);

 setFlags_Widget(sheet,

                     arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag |

                     centerHorizontal_WidgetFlag | overflowScrollable_WidgetFlag,

                 iTrue);

@@ -772,7 +847,7 @@ iWidget *makeSheet_Widget(const char *id) {

}

void centerSheet_Widget(iWidget *sheet) {

 postRefresh_App();

}

@@ -833,7 +908,7 @@ iBool valueInputHandler_(iWidget *dlg, const char *cmd) {

     return iFalse;

 }

 if (equal_Command(cmd, "input.ended")) {

         if (arg_Command(cmd)) {

             acceptValueInput_(dlg);

         }

@@ -860,11 +935,62 @@ iBool valueInputHandler_(iWidget *dlg, const char *cmd) {

 return iFalse;

}

+static iWidget *makeDialogButtons_(const iMenuItem *actions, size_t numActions) {

+}

iWidget *makeValueInput_Widget(iWidget *parent, const iString *initialValue, const char *title,

                            const char *prompt, const char *acceptLabel, const char *command) {

 if (parent) {

     setFocus_Widget(NULL);

-// processEvents_App(postedEventsOnly_AppEventMode);

 }

 iWidget *dlg = makeSheet_Widget(command);

 setCommandHandler_Widget(dlg, valueInputHandler_);

@@ -884,18 +1010,11 @@ iWidget *makeValueInput_Widget(iWidget *parent, const iString *initialValue, con

 setId_Widget(as_Widget(input), "input");

 updateValueInputWidth_(dlg);

 addChild_Widget(dlg, iClob(makePadding_Widget(gap_UI)));

 centerSheet_Widget(dlg);

 if (parent) {

     setFocus_Widget(as_Widget(input));

@@ -914,42 +1033,35 @@ static iBool messageHandler_(iWidget *msg, const char *cmd) {

 if (!(equal_Command(cmd, "media.updated") ||

       equal_Command(cmd, "media.player.update") ||

       equal_Command(cmd, "bookmarks.request.finished") ||

     destroy_Widget(msg);

 }

 return iFalse;

}

iWidget *makeMessage_Widget(const char *title, const char *msg) {

 addAction_Widget(dlg, SDLK_ESCAPE, 0, "message.ok");

 addAction_Widget(dlg, SDLK_SPACE, 0, "message.ok");

 return dlg;

}

-iWidget *makeQuestion_Widget(const char *title, const char *msg, const char *labels[],

+iWidget *makeQuestion_Widget(const char *title, const char *msg,

 processEvents_App(postedEventsOnly_AppEventMode);

 iWidget *dlg = makeSheet_Widget("");

 setCommandHandler_Widget(dlg, messageHandler_);

 addChildFlags_Widget(dlg, iClob(new_LabelWidget(title, NULL)), frameless_WidgetFlag);

 addChildFlags_Widget(dlg, iClob(new_LabelWidget(msg, NULL)), frameless_WidgetFlag);

 addChild_Widget(dlg, iClob(makePadding_Widget(gap_UI)));

 addChild_Widget(get_Window()->root, iClob(dlg));

 centerSheet_Widget(dlg);

 return dlg;

}

@@ -1037,14 +1149,18 @@ static void addRadioButton_(iWidget *parent, const char *id, const char *label,

static void addFontButtons_(iWidget *parent, const char *id) {

 const char *fontNames[] = {

 };

 iForIndices(i, fontNames) {

 }

+// setFrameColor_Widget(findChild_Widget(as_Widget(button), "menu"), uiBackgroundSelected_ColorId);

}

iWidget *makePreferences_Widget(void) {

@@ -1063,6 +1179,9 @@ iWidget *makePreferences_Widget(void) {

#endif

     addChild_Widget(headings, iClob(makeHeading_Widget("Show URL on hover:")));

     addChild_Widget(values, iClob(makeToggle_Widget("prefs.hoverlink")));

     addChild_Widget(headings, iClob(makeHeading_Widget("Smooth scrolling:")));

     addChild_Widget(values, iClob(makeToggle_Widget("prefs.smoothscroll")));

     addChild_Widget(headings, iClob(makeHeading_Widget("Load image on scroll:")));

@@ -1083,10 +1202,22 @@ iWidget *makePreferences_Widget(void) {

         setId_Widget(addChild_Widget(themes, iClob(new_LabelWidget("Pure White", "theme.set arg:3"))), "prefs.theme.3");

     }

     addChildFlags_Widget(values, iClob(themes), arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag);

+#if defined (LAGRANGE_CUSTOM_FRAME)

+#endif

     addChild_Widget(headings, iClob(makeHeading_Widget("UI scale factor:")));

     setId_Widget(addChild_Widget(values, iClob(new_InputWidget(8))), "prefs.uiscale");

     makeTwoColumnHeading_("WIDE LAYOUT", headings, values);

     addChild_Widget(headings, iClob(makeHeading_Widget("Site icon:")));

     addChild_Widget(values, iClob(makeToggle_Widget("prefs.sideicon")));

@@ -1107,18 +1238,20 @@ iWidget *makePreferences_Widget(void) {

             { "High Contrast", 0, 0, format_CStr("doctheme.%s.set arg:%d", mode, highContrast_GmDocumentTheme) },

         };

         addChild_Widget(headings, iClob(makeHeading_Widget(isDark ? "Dark theme:" : "Light theme:")));

+// setFrameColor_Widget(findChild_Widget(as_Widget(button), "menu"),

+// uiBackgroundSelected_ColorId);

                      format_CStr("prefs.doctheme.%s", mode));

     }

     addChild_Widget(headings, iClob(makeHeading_Widget("Saturation:")));

     iWidget *sats = new_Widget();

     /* Saturation levels. */ {

     }

     addChildFlags_Widget(values, iClob(sats), arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag);

 }

@@ -1128,13 +1261,13 @@ iWidget *makePreferences_Widget(void) {

     /* Fonts. */ {

         iWidget *fonts;

         addChild_Widget(headings, iClob(makeHeading_Widget("Heading font:")));

+// fonts = new_Widget();

+// addChildFlags_Widget(values, iClob(fonts), 0); //arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag);

         addChild_Widget(headings, iClob(makeHeading_Widget("Body font:")));

+// fonts = new_Widget();

+// addChildFlags_Widget(values, iClob(fonts), 0); //arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag);

         addChild_Widget(headings, iClob(makeHeading_Widget("Monospace body:")));

         iWidget *mono = new_Widget();

         /* TODO: Needs labels! */

@@ -1167,6 +1300,10 @@ iWidget *makePreferences_Widget(void) {

 }

 /* Network. */ {

     appendTwoColumnPage_(tabs, "Network", '5', &headings, &values);

     addChild_Widget(headings, iClob(makeHeading_Widget("Cache size:")));

     iWidget *cacheGroup = new_Widget(); {

         iInputWidget *cache = new_InputWidget(4);

@@ -1175,8 +1312,6 @@ iWidget *makePreferences_Widget(void) {

         addChildFlags_Widget(cacheGroup, iClob(new_LabelWidget("MB", NULL)), frameless_WidgetFlag);

     }

     addChildFlags_Widget(values, iClob(cacheGroup), arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag);

     makeTwoColumnHeading_("PROXIES", headings, values);

     addChild_Widget(headings, iClob(makeHeading_Widget("Gemini proxy:")));

     setId_Widget(addChild_Widget(values, iClob(new_InputWidget(0))), "prefs.proxy.gemini");

@@ -1190,24 +1325,21 @@ iWidget *makePreferences_Widget(void) {

     setFlags_Widget(as_Widget(bind), borderTop_WidgetFlag, iTrue);

     appendFramelessTabPage_(tabs, iClob(bind), "Keys", '6', KMOD_PRIMARY);

 }

 resizeToLargestPage_Widget(tabs);

 arrange_Widget(dlg);

 /* Set input field sizes. */ {

     expandInputFieldWidth_(findChild_Widget(tabs, "prefs.downloads"));

     expandInputFieldWidth_(findChild_Widget(tabs, "prefs.proxy.gemini"));

     expandInputFieldWidth_(findChild_Widget(tabs, "prefs.proxy.gopher"));

     expandInputFieldWidth_(findChild_Widget(tabs, "prefs.proxy.http"));

 }

 addChild_Widget(get_Window()->root, iClob(dlg));

 return dlg;

}

@@ -1225,7 +1357,7 @@ iWidget *makeBookmarkEditor_Widget(void) {

     page, iClob(new_Widget()), arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag);

 iWidget *values = addChildFlags_Widget(

     page, iClob(new_Widget()), arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag);

 addChild_Widget(headings, iClob(makeHeading_Widget("Title:")));

 setId_Widget(addChild_Widget(values, iClob(inputs[0] = new_InputWidget(0))), "bmed.title");

 addChild_Widget(headings, iClob(makeHeading_Widget("URL:")));

@@ -1233,20 +1365,20 @@ iWidget *makeBookmarkEditor_Widget(void) {

 setUrlContent_InputWidget(inputs[1], iTrue);

 addChild_Widget(headings, iClob(makeHeading_Widget("Tags:")));

 setId_Widget(addChild_Widget(values, iClob(inputs[2] = new_InputWidget(0))), "bmed.tags");

 arrange_Widget(dlg);

 for (int i = 0; i < 3; ++i) {

     as_Widget(inputs[i])->rect.size.x = 100 * gap_UI - headings->rect.size.x;

 }

 addChild_Widget(get_Window()->root, iClob(dlg));

 centerSheet_Widget(dlg);

 return dlg;

@@ -1263,11 +1395,14 @@ static iBool handleBookmarkCreationCommands_SidebarWidget_(iWidget *editor, cons

         const iString *title = text_InputWidget(findChild_Widget(editor, "bmed.title"));

         const iString *url   = text_InputWidget(findChild_Widget(editor, "bmed.url"));

         const iString *tags  = text_InputWidget(findChild_Widget(editor, "bmed.tags"));

         postCommand_App("bookmarks.changed");

     }

     destroy_Widget(editor);

@@ -1370,20 +1505,17 @@ iWidget *makeFeedSettings_Widget(uint32_t bookmarkId) {

     addRadioButton_(types, "feedcfg.type.headings", "New Headings", "feedcfg.type arg:1");

 }

 addChildFlags_Widget(values, iClob(types), arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag);

 arrange_Widget(dlg);

 as_Widget(input)->rect.size.x = 100 * gap_UI - headings->rect.size.x;

 addChild_Widget(get_Window()->root, iClob(dlg));

@@ -1455,16 +1587,14 @@ iWidget *makeIdentityCreation_Widget(void) {

 for (size_t i = 0; i < iElemCount(inputs); ++i) {

     as_Widget(inputs[i])->rect.size.x = 100 * gap_UI - headings->rect.size.x;

 }

 addChild_Widget(get_Window()->root, iClob(dlg));

 centerSheet_Widget(dlg);

 return dlg;

diff --git a/src/ui/util.h b/src/ui/util.h

index a280fedb..96e08e95 100644

--- a/src/ui/util.h

+++ b/src/ui/util.h

@@ -75,7 +75,8 @@ enum iAnimFlag {

 indefinite_AnimFlag = iBit(1), /* does not end; must be linear */

 easeIn_AnimFlag     = iBit(2),

 easeOut_AnimFlag    = iBit(3),

};

struct Impl_Anim {

@@ -198,7 +199,7 @@ iWidget * makeValueInput_Widget (iWidget *parent, const iString *initialValu

void updateValueInput_Widget (iWidget *, const char *title, const char *prompt);

iWidget * makeMessage_Widget (const char *title, const char *msg);

iWidget * makeQuestion_Widget (const char *title, const char *msg,

iWidget * makePreferences_Widget (void);

iWidget * makeBookmarkEditor_Widget (void);

diff --git a/src/ui/widget.c b/src/ui/widget.c

index ddb3f092..386ba6d6 100644

--- a/src/ui/widget.c

+++ b/src/ui/widget.c

@@ -23,6 +23,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */

#include "widget.h"

#include "app.h"

+#include "touch.h"

#include "command.h"

#include "paint.h"

#include "util.h"

@@ -77,6 +78,7 @@ void init_Widget(iWidget *d) {

 d->rect           = zero_Rect();

 d->bgColor        = none_ColorId;

 d->frameColor     = none_ColorId;

 d->children       = NULL;

 d->parent         = NULL;

 d->commandHandler = NULL;

@@ -86,6 +88,11 @@ void init_Widget(iWidget *d) {

void deinit_Widget(iWidget *d) {

 releaseChildren_Widget(d);

 deinit_String(&d->id);

+// printf("widget %p deleted (on top:%d)\n", d, d->flags & keepOnTop_WidgetFlag ? 1 : 0);

}

static void aboutToBeDestroyed_Widget_(iWidget *d) {

@@ -126,11 +133,15 @@ const iString *id_Widget(const iWidget *d) {

}

int64_t flags_Widget(const iWidget *d) {

}

void setFlags_Widget(iWidget *d, int64_t flags, iBool set) {

 if (d) {

     iChangeFlags(d->flags, flags, set);

     if (flags & keepOnTop_WidgetFlag) {

         if (set) {

@@ -149,6 +160,8 @@ void setPos_Widget(iWidget *d, iInt2 pos) {

}

void setSize_Widget(iWidget *d, iInt2 size) {

 d->rect.size = size;

 setFlags_Widget(d, fixedSize_WidgetFlag, iTrue);

}

@@ -160,8 +173,33 @@ void setPadding_Widget(iWidget *d, int left, int top, int right, int bottom) {

 d->padding[3] = bottom;

}

+static void visualOffsetAnimation_Widget_(void *ptr) {

+}

+void setVisualOffset_Widget(iWidget *d, int value, uint32_t span, int animFlags) {

+}

void setBackgroundColor_Widget(iWidget *d, int bgColor) {

}

void setFrameColor_Widget(iWidget *d, int frameColor) {

@@ -296,7 +334,7 @@ void arrange_Widget(iWidget *d) {

         iWidget *child = as_Widget(c.object);

         if (isCollapsed_Widget_(child)) {

             if (d->flags & arrangeHorizontal_WidgetFlag) {

             }

             if (d->flags & arrangeVertical_WidgetFlag) {

                 setHeight_Widget_(child, 0);

@@ -331,8 +369,8 @@ void arrange_Widget(iWidget *d) {

         avail = divi_I2(max_I2(zero_I2(), avail), expCount);

         iForEach(ObjectList, j, d->children) {

             iWidget *child = as_Widget(j.object);

                 continue;

             }

             if (child->flags & expand_WidgetFlag) {

@@ -367,7 +405,7 @@ void arrange_Widget(iWidget *d) {

         }

         iForEach(ObjectList, i, d->children) {

             iWidget *child = as_Widget(i.object);

                 if (dirs.x) setWidth_Widget_(child, childSize.x);

                 if (dirs.y) setHeight_Widget_(child, childSize.y);

             }

@@ -450,8 +488,15 @@ void arrange_Widget(iWidget *d) {

iRect bounds_Widget(const iWidget *d) {

 iRect bounds = d->rect;

 for (const iWidget *w = d->parent; w; w = w->parent) {

 }

 return bounds;

}

@@ -623,7 +668,9 @@ iBool processEvent_Widget(iWidget *d, const SDL_Event *ev) {

}

void drawBackground_Widget(const iWidget *d) {

 if (flags_Widget(d) & borderTop_WidgetFlag) {

     const iRect rect = bounds_Widget(d);

     iPaint p;

@@ -631,10 +678,56 @@ void drawBackground_Widget(const iWidget *d) {

     drawHLine_Paint(&p, topLeft_Rect(rect), width_Rect(rect), uiBackgroundFramelessHover_ColorId);

 }

 if (d->bgColor >= 0 || d->frameColor >= 0) {

     iPaint p;

     init_Paint(&p);

     if (d->bgColor >= 0) {

+#if defined (iPlatformAppleMobile)

+#endif

         fillRect_Paint(&p, rect, d->bgColor);

     }

     if (d->frameColor >= 0 && ~d->flags & frameless_WidgetFlag) {

@@ -643,11 +736,17 @@ void drawBackground_Widget(const iWidget *d) {

 }

}

+iLocalDef iBool isDrawn_Widget_(const iWidget *d) {

+}

void drawChildren_Widget(const iWidget *d) {

 iConstForEach(ObjectList, i, d->children) {

     const iWidget *child = constAs_Widget(i.object);

         class_Widget(child)->draw(child);

     }

 }

@@ -686,6 +785,28 @@ iAny *addChildPos_Widget(iWidget *d, iAnyObject *child, enum iWidgetAddPos addPo

 return child;

}

+iAny *insertChildAfter_Widget(iWidget *d, iAnyObject *child, size_t afterIndex) {

+}

+iAny *insertChildAfterFlags_Widget(iWidget *d, iAnyObject *child, size_t afterIndex, int64_t childFlags) {

+}

iAny *addChildFlags_Widget(iWidget *d, iAnyObject *child, int64_t childFlags) {

 setFlags_Widget(child, childFlags, iTrue);

 return addChild_Widget(d, child);

@@ -729,11 +850,27 @@ size_t childIndex_Widget(const iWidget *d, const iAnyObject *child) {

}

iAny *hitChild_Widget(const iWidget *d, iInt2 coord) {

 }

     return iConstCast(iWidget *, d);

 }

 return NULL;

diff --git a/src/ui/widget.h b/src/ui/widget.h

index 79f68b3c..c5a1a360 100644

--- a/src/ui/widget.h

+++ b/src/ui/widget.h

@@ -25,6 +25,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */

/* Base class for UI widgets. */

#include "metrics.h"

+#include "util.h"

#include <the_Foundation/object.h>

#include <the_Foundation/objectlist.h>

@@ -91,6 +92,14 @@ enum iWidgetFlag {

#define borderTop_WidgetFlag iBit64(37)

#define overflowScrollable_WidgetFlag iBit64(38)

#define focusRoot_WidgetFlag iBit64(39)

+#define unhittable_WidgetFlag iBit64(40)

+#define touchDrag_WidgetFlag iBit64(41) /* touch event behavior: immediate drag */

+#define noBackground_WidgetFlag iBit64(42)

+#define drawBackgroundToHorizontalSafeArea_WidgetFlag iBit64(43)

+#define drawBackgroundToVerticalSafeArea_WidgetFlag iBit64(44)

+#define visualOffset_WidgetFlag iBit64(45)

+#define parentCannotResize_WidgetFlag iBit64(46)

+#define noTopFrame_WidgetFlag iBit64(47)

enum iWidgetAddPos {

 back_WidgetAddPos,

@@ -108,6 +117,7 @@ struct Impl_Widget {

 int64_t      flags;

 iRect        rect;

 int          padding[4]; /* left, top, right, bottom */

 int          bgColor;

 int          frameColor;

 iObjectList *children;

@@ -157,12 +167,18 @@ void drawBackground_Widget(const iWidget *);

void drawChildren_Widget (const iWidget *);

iLocalDef int width_Widget(const iAnyObject *d) {

}

iLocalDef int height_Widget(const iAnyObject *d) {

}

iLocalDef iObjectList *children_Widget(iAnyObject *d) {

 iAssert(isInstance_Object(d, &Class_Widget));

@@ -189,12 +205,15 @@ void setPos_Widget (iWidget *, iInt2 pos);

void setSize_Widget (iWidget *, iInt2 size);

void setPadding_Widget (iWidget *, int left, int top, int right, int bottom);

iLocalDef void setPadding1_Widget (iWidget *d, int padding) { setPadding_Widget(d, padding, padding, padding, padding); }

+void setVisualOffset_Widget (iWidget *d, int value, uint32_t span, int animFlags);

void setBackgroundColor_Widget (iWidget *, int bgColor);

void setFrameColor_Widget (iWidget *, int frameColor);

void setCommandHandler_Widget (iWidget *, iBool (*handler)(iWidget *, const char *));

-iAny * addChild_Widget (iWidget *, iAnyObject child); / holds a ref */

-iAny * addChildPos_Widget (iWidget *, iAnyObject *child, enum iWidgetAddPos addPos);

-iAny * addChildFlags_Widget(iWidget *, iAnyObject child, int64_t childFlags); / holds a ref */

+iAny * addChild_Widget (iWidget *, iAnyObject child); / holds a ref */

+iAny * addChildPos_Widget (iWidget *, iAnyObject *child, enum iWidgetAddPos addPos);

+iAny * addChildFlags_Widget (iWidget *, iAnyObject child, int64_t childFlags); / holds a ref */

+iAny * insertChildAfter_Widget (iWidget *, iAnyObject *child, size_t afterIndex);

+iAny * insertChildAfterFlags_Widget(iWidget *, iAnyObject *child, size_t afterIndex, int64_t childFlags);

iAny * removeChild_Widget (iWidget *, iAnyObject child); / returns a ref */

iAny * child_Widget (iWidget , size_t index); / O(n) */

size_t childIndex_Widget (const iWidget *, const iAnyObject child); / O(n) */

diff --git a/src/ui/window.c b/src/ui/window.c

index 3958f682..09238b2a 100644

--- a/src/ui/window.c

+++ b/src/ui/window.c

@@ -33,6 +33,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */

#include "paint.h"

#include "util.h"

#include "keys.h"

+#include "touch.h"

#include "../app.h"

#include "../visited.h"

#include "../gmcerts.h"

@@ -41,18 +42,24 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */

#if defined (iPlatformMsys)

include "../win32.h"

#endif

-#if defined (iPlatformApple) && !defined (iPlatformIOS)

+#if defined (iPlatformAppleDesktop)

include "macos.h"

#endif

+#if defined (iPlatformAppleMobile)

+# include "ios.h"

+#endif

#include <the_Foundation/file.h>

#include <the_Foundation/path.h>

+#include <the_Foundation/regexp.h>

#include <SDL_hints.h>

#include <SDL_timer.h>

#include <SDL_syswm.h>

#define STB_IMAGE_IMPLEMENTATION

+#define STB_IMAGE_RESIZE_IMPLEMENTATION

#include "stb_image.h"

+#include "stb_image_resize.h"

static iWindow *theWindow_ = NULL;

@@ -84,6 +91,59 @@ static iBool handleRootCommands_(iWidget *root, const char *cmd) {

 }

 else if (equal_Command(cmd, "window.focus.lost")) {

     setFocus_Widget(NULL);

     return iFalse;

 }

 else if (handleCommand_App(cmd)) {

@@ -92,11 +152,12 @@ static iBool handleRootCommands_(iWidget *root, const char *cmd) {

 return iFalse;

}

-#if defined (iPlatformApple)

+#if defined (iPlatformAppleDesktop)

define iHaveNativeMenus

#endif

#if !defined (iHaveNativeMenus)

+#if !defined (iPlatformAppleMobile)

/* TODO: Submenus wouldn't hurt here. */

static const iMenuItem navMenuItems_[] = {

 { "New Tab", 't', KMOD_PRIMARY, "tabs.new" },

@@ -123,6 +184,48 @@ static const iMenuItem navMenuItems_[] = {

 { "---", 0, 0, NULL },

 { "Quit Lagrange", 'q', KMOD_PRIMARY, "quit" }

};

+#else

+/* Tablet menu. */

+static const iMenuItem tabletNavMenuItems_[] = {

+};

+/* Phone menu. */

+static const iMenuItem phoneNavMenuItems_[] = {

+};

+#endif /* AppleMobile */

#endif

#if defined (iHaveNativeMenus)

@@ -190,20 +293,31 @@ static const iMenuItem helpMenuItems_[] = {

};

#endif

+#if defined (iPlatformAppleMobile)

static const iMenuItem identityButtonMenuItems_[] = {

 { "No Active Identity", 0, 0, "ident.showactive" },

 { "---", 0, 0, NULL },

-#if !defined (iHaveNativeMenus)

+};

+#else /* desktop */

+static const iMenuItem identityButtonMenuItems_[] = {

+# if !defined (iHaveNativeMenus)

 { "New Identity...", SDLK_n, KMOD_PRIMARY | KMOD_SHIFT, "ident.new" },

 { "Import...", SDLK_i, KMOD_PRIMARY | KMOD_SHIFT, "ident.import" },

 { "---", 0, 0, NULL },

 { "Show Identities", '4', KMOD_PRIMARY, "sidebar.mode arg:3 show:1" },

-#else

+# else

 { "New Identity...", 0, 0, "ident.new" },

 { "---", 0, 0, NULL },

 { "Show Identities", 0, 0, "sidebar.mode arg:3 show:1" },

#endif

};

+#endif

static const char *reloadCStr_ = "\U0001f503";

@@ -291,7 +405,9 @@ static void updateNavBarIdentity_(iWidget *navBar) {

 const iGmIdentity *ident =

     identityForUrl_GmCerts(certs_App(), url_DocumentWidget(document_App()));

 iWidget *button = findChild_Widget(navBar, "navbar.ident");

 setFlags_Widget(button, selected_WidgetFlag, ident != NULL);

 /* Update menu. */

 iLabelWidget *idItem = child_Widget(findChild_Widget(button, "menu"), 0);

 setTextCStr_LabelWidget(

@@ -317,8 +433,8 @@ static uint32_t updateReloadAnimation_Window_(uint32_t interval, void *window) {

}

static void setReloadLabel_Window_(iWindow *d, iBool animating) {

}

static void checkLoadAnimation_Window_(iWindow *d) {

@@ -333,22 +449,113 @@ static void checkLoadAnimation_Window_(iWindow *d) {

 setReloadLabel_Window_(d, isOngoing);

}

+static void updatePadding_Window_(iWindow *d) {

+#if defined (iPlatformAppleMobile)

+#endif

+}

+static void dismissPortraitPhoneSidebars_(void) {

+// setFlags_Widget(findWidget_App("toolbar.ident"), noBackground_WidgetFlag, iTrue);

+// setFlags_Widget(findWidget_App("toolbar.view"), noBackground_WidgetFlag, iTrue);

+}

+static iBool willPerformSearchQuery_(const iString *userInput) {

+}

+static void showSearchQueryIndicator_(iBool show) {

+}

+static int navBarAvailableSpace_(iWidget *navBar) {

+}

static iBool handleNavBarCommands_(iWidget *navBar, const char *cmd) {

 if (equal_Command(cmd, "window.resized")) {

     if (isNarrow ^ ((flags_Widget(navBar) & tight_WidgetFlag) != 0)) {

         setFlags_Widget(navBar, tight_WidgetFlag, isNarrow);

         iForEach(ObjectList, i, navBar->children) {

             iWidget *child = as_Widget(i.object);

             setFlags_Widget(

             if (isInstance_Object(i.object, &Class_LabelWidget)) {

                 iLabelWidget *label = i.object;

                 updateSize_LabelWidget(label);

             }

         }

     }

     refresh_Widget(navBar);

     postCommand_Widget(navBar, "layout.changed id:navbar");

     return iFalse;

@@ -368,14 +575,18 @@ static iBool handleNavBarCommands_(iWidget *navBar, const char *cmd) {

     return iTrue;

 }

 else if (equal_Command(cmd, "input.edited")) {

     if (pointer_Command(cmd) == url) {

         return iTrue;

     }

 }

 else if (startsWith_CStr(cmd, "input.ended id:url ")) {

     iInputWidget *url = findChild_Widget(navBar, "url");

     if (isEmpty_String(text_InputWidget(url))) {

         /* User entered nothing; restore the current URL. */

         setText_InputWidget(url, url_DocumentWidget(document_App()));

@@ -385,9 +596,14 @@ static iBool handleNavBarCommands_(iWidget *navBar, const char *cmd) {

         !isFocused_Widget(findWidget_App("lookup"))) {

         iString *newUrl = copy_String(text_InputWidget(url));

         trim_String(newUrl);

         return iTrue;

     }

 }

@@ -402,12 +618,11 @@ static iBool handleNavBarCommands_(iWidget *navBar, const char *cmd) {

             postCommand_App("visited.changed"); /* sidebar will update */

             setText_InputWidget(url, urlStr);

             checkLoadAnimation_Window_(get_Window());

             updateNavBarIdentity_(navBar);

             /* Icon updates should be limited to automatically chosen icons if the user

                is allowed to pick their own in the future. */

                     siteIcon_GmDocument(document_DocumentWidget(document_App())))) {

                 postCommand_App("bookmarks.changed");

             }

@@ -421,6 +636,7 @@ static iBool handleNavBarCommands_(iWidget *navBar, const char *cmd) {

             iInputWidget *url = findChild_Widget(navBar, "url");

             setTextCStr_InputWidget(url, suffixPtr_Command(cmd, "url"));

             checkLoadAnimation_Window_(get_Window());

             return iFalse;

         }

     }

@@ -503,7 +719,97 @@ static iBool handleSearchBarCommands_(iWidget *searchBar, const char *cmd) {

 return iFalse;

}

+#if defined (iPlatformAppleMobile)

+static void dismissSidebar_(iWidget *sidebar, const char *toolButtonId) {

+// setFlags_Widget(findWidget_App(toolButtonId), noBackground_WidgetFlag, iTrue);

+}

+static iBool handleToolBarCommands_(iWidget *toolBar, const char *cmd) {

+// setFlags_Widget(findChild_Widget(toolBar, "toolbar.view"), noBackground_WidgetFlag,

+// isVisible);

+// setFlags_Widget(findChild_Widget(toolBar, "toolbar.ident"), noBackground_WidgetFlag,

+// isVisible);

+}

+#endif /* defined (iPlatformAppleMobile) */

+static iLabelWidget *newLargeIcon_LabelWidget(const char *text, const char *cmd) {

+}

+static int appIconSize_(void) {

+}

static void setupUserInterface_Window(iWindow *d) {

 /* Children of root cover the entire window. */

 setFlags_Widget(d->root, resizeChildren_WidgetFlag, iTrue);

 setCommandHandler_Widget(d->root, handleRootCommands_);

@@ -512,32 +818,79 @@ static void setupUserInterface_Window(iWindow *d) {

 setId_Widget(div, "navdiv");

 addChild_Widget(d->root, iClob(div));

+#if defined (LAGRANGE_CUSTOM_FRAME)

+#endif

 /* Navigation bar. */ {

     iWidget *navBar = new_Widget();

     setId_Widget(navBar, "navbar");

     setFlags_Widget(navBar,

                     iTrue);

     addChild_Widget(div, iClob(navBar));

     setBackgroundColor_Widget(navBar, uiBackground_ColorId);

     setCommandHandler_Widget(navBar, handleNavBarCommands_);

     iLabelWidget *idMenu = makeMenuButton_LabelWidget(

         "\U0001f464", identityButtonMenuItems_, iElemCount(identityButtonMenuItems_));

     setAlignVisually_LabelWidget(idMenu, iTrue);

     /* URL input field. */ {

         iInputWidget *url = new_InputWidget(0);

         setSelectAllOnFocus_InputWidget(url, iTrue);

@@ -545,8 +898,19 @@ static void setupUserInterface_Window(iWindow *d) {

         setUrlContent_InputWidget(url, iTrue);

         setNotifyEdits_InputWidget(url, iTrue);

         setTextCStr_InputWidget(url, "gemini://");

         /* Feeds refresh indicator is inside the input field. */ {

             iLabelWidget *fprog = new_LabelWidget(uiTextCaution_ColorEscape

                                                   "\u2605 Refreshing Feeds...", NULL);

@@ -569,18 +933,39 @@ static void setupUserInterface_Window(iWindow *d) {

                                  iClob(progress),

                                  moveToParentRightEdge_WidgetFlag);

         }

     }

     setId_Widget(addChild_Widget(

                      navBar, iClob(newIcon_LabelWidget(reloadCStr_, 0, 0, "navigate.reload"))),

                  "reload");

                     iClob(newIcon_LabelWidget(

#if !defined (iHaveNativeMenus)

+# if defined (iPlatformAppleMobile)

+# else

     iLabelWidget *navMenu =

         makeMenuButton_LabelWidget("\U0001d362", navMenuItems_, iElemCount(navMenuItems_));

+# endif

     setAlignVisually_LabelWidget(navMenu, iTrue);

#else

     insertMenuItems_MacOS("File", 1, fileMenuItems_, iElemCount(fileMenuItems_));

     insertMenuItems_MacOS("Edit", 2, editMenuItems_, iElemCount(editMenuItems_));

@@ -591,13 +976,20 @@ static void setupUserInterface_Window(iWindow *d) {

#endif

 }

 /* Tab bar. */ {

     setId_Widget(tabBar, "doctabs");

     setBackgroundColor_Widget(tabBar, uiBackground_ColorId);

     appendTabPage_Widget(tabBar, iClob(new_DocumentWidget()), "Document", 0, 0);

     iWidget *buttons = findChild_Widget(tabBar, "tabs.buttons");

     setId_Widget(

         addChild_Widget(buttons, iClob(newIcon_LabelWidget("\u2795", 0, 0, "tabs.new"))),

         "newtab");

@@ -607,7 +999,14 @@ static void setupUserInterface_Window(iWindow *d) {

     iSidebarWidget *sidebar1 = new_SidebarWidget(left_SideBarSide);

     addChildPos_Widget(content, iClob(sidebar1), front_WidgetAddPos);

     iSidebarWidget *sidebar2 = new_SidebarWidget(right_SideBarSide);

 }

 /* Lookup results. */ {

     iLookupWidget *lookup = new_LookupWidget();

@@ -635,6 +1034,43 @@ static void setupUserInterface_Window(iWindow *d) {

     addChild_Widget(searchBar, iClob(newIcon_LabelWidget("  \u2b9d  ", 'g', KMOD_PRIMARY | KMOD_SHIFT, "find.prev")));

     addChild_Widget(searchBar, iClob(newIcon_LabelWidget("\u2a2f", SDLK_ESCAPE, 0, "find.close")));

 }

+#if defined (iPlatformAppleMobile)

+// setBackgroundColor_Widget(i.object, tmBannerSideTitle_ColorId);

+#endif

 iWidget *tabsMenu = makeMenu_Widget(d->root,

                                     (iMenuItem[]){

                                         { "Close Tab", 0, 0, "tabs.close" },

@@ -667,8 +1103,8 @@ static void updateRootSize_Window_(iWindow *d, iBool notifyAlways) {

 const iInt2 oldSize = *size;

 SDL_GetRendererOutputSize(d->render, &size->x, &size->y);

 if (notifyAlways || !isEqual_I2(oldSize, *size)) {

     arrange_Widget(d->root);

     postCommandf_App("window.resized width:%d height:%d horiz:%d vert:%d",

                      size->x,

@@ -676,7 +1112,7 @@ static void updateRootSize_Window_(iWindow *d, iBool notifyAlways) {

                      isHoriz,

                      isVert);

     postRefresh_App();

 }

}

@@ -712,29 +1148,116 @@ static void drawBlank_Window_(iWindow *d) {

 SDL_RenderPresent(d->render);

}

+#if defined (LAGRANGE_CUSTOM_FRAME)

+static SDL_HitTestResult hitTest_Window_(SDL_Window *win, const SDL_Point *pos, void *data) {

+}

+SDL_HitTestResult hitTest_Window(const iWindow *d, iInt2 pos) {

+}

+#endif

iBool create_Window_(iWindow *d, iRect rect, uint32_t flags) {

 flags |= SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI | SDL_WINDOW_HIDDEN;

+#if defined (LAGRANGE_CUSTOM_FRAME)

+#endif

 if (SDL_CreateWindowAndRenderer(

         width_Rect(rect), height_Rect(rect), flags, &d->win, &d->render)) {

     return iFalse;

 }

+#if defined (LAGRANGE_CUSTOM_FRAME)

+#endif

 return iTrue;

}

+#if defined (iPlatformLinux) || defined (LAGRANGE_CUSTOM_FRAME)

+static SDL_Surface *loadAppIconSurface_(int resized) {

+}

+#endif

void init_Window(iWindow *d, iRect rect) {

 theWindow_ = d;

 d->win = NULL;

 iZap(d->cursors);

 d->pendingCursor = NULL;

 d->isDrawFrozen = iTrue;

 d->isMouseInside = iTrue;

 d->focusGainedAt = 0;

 uint32_t flags = 0;

-#if defined (iPlatformApple)

+#if defined (iPlatformAppleDesktop)

 SDL_SetHint(SDL_HINT_RENDER_DRIVER, shouldDefaultToMetalRenderer_MacOS() ? "metal" : "opengl");

+#elif defined (iPlatformAppleMobile)

#else

 flags |= SDL_WINDOW_OPENGL;

#endif

@@ -751,7 +1274,7 @@ void init_Window(iWindow *d, iRect rect) {

 if (left_Rect(rect) >= 0 || top_Rect(rect) >= 0) {

     SDL_SetWindowPosition(d->win, left_Rect(rect), top_Rect(rect));

 }

 SDL_SetWindowMinimumSize(d->win, minSize.x, minSize.y);

 SDL_SetWindowTitle(d->win, "Lagrange");

 /* Some info. */ {

@@ -774,26 +1297,23 @@ void init_Window(iWindow *d, iRect rect) {

 d->pixelRatio = pixelRatio_Window_(d);

 setPixelRatio_Metrics(d->pixelRatio * d->uiScale);

#if defined (iPlatformMsys)

 SDL_SetWindowMinimumSize(d->win, minSize.x * d->pixelRatio, minSize.y * d->pixelRatio);

 useExecutableIconResource_SDLWindow(d->win);

#endif

#if defined (iPlatformLinux)

 SDL_SetWindowMinimumSize(d->win, minSize.x * d->pixelRatio, minSize.y * d->pixelRatio);

 /* Load the window icon. */ {

     SDL_SetWindowIcon(d->win, surf);

     SDL_FreeSurface(surf);

 }

+#endif

+#if defined (iPlatformAppleMobile)

#endif

 d->root = new_Widget();

 setFlags_Widget(d->root, focusRoot_WidgetFlag, iTrue);

@@ -805,6 +1325,19 @@ void init_Window(iWindow *d, iRect rect) {

 setupUserInterface_Window(d);

 postCommand_App("bindings.changed"); /* update from bindings */

 updateRootSize_Window_(d, iFalse);

+#if defined (LAGRANGE_CUSTOM_FRAME)

+#endif

}

void deinit_Window(iWindow *d) {

@@ -827,48 +1360,162 @@ SDL_Renderer *renderer_Window(const iWindow *d) {

 return d->render;

}

-static iBool isMaximized_Window_(const iWindow *d) {

-#if !defined (iPlatformApple)

-#else

+iBool isFullscreen_Window(const iWindow *d) {

+}

+static void invalidate_Window_(iWindow *d) {

+}

+static iBool isNormalPlacement_Window_(const iWindow *d) {

+#if defined (iPlatformApple)

+#endif

+}

+static iBool unsnap_Window_(iWindow *d, const iInt2 *newPos) {

+#if defined (LAGRANGE_CUSTOM_FRAME)

#endif

}

static iBool handleWindowEvent_Window_(iWindow *d, const SDL_WindowEvent *ev) {

 switch (ev->event) {

     case SDL_WINDOWEVENT_EXPOSED:

         /* Since we are manually controlling when to redraw the window, we are responsible

            for ensuring that window contents get redrawn after expose events. Under certain

            circumstances (e.g., under openbox), not doing this would mean that the window

            is missing contents until other events trigger a refresh. */

         postRefresh_App();

#if defined (LAGRANGE_ENABLE_WINDOWPOS_FIX)

             int bx, by;

             SDL_GetWindowBordersSize(d->win, &by, &bx, NULL, NULL);

         }

#endif

         return iFalse;

     case SDL_WINDOWEVENT_MOVED: {

+#if defined (LAGRANGE_CUSTOM_FRAME)

+#endif

             iInt2 border = zero_I2();

#if !defined (iPlatformApple)

             SDL_GetWindowBordersSize(d->win, &border.y, &border.x, NULL, NULL);

#endif

         }

         return iTrue;

     }

     case SDL_WINDOWEVENT_RESIZED:

         }

         updateRootSize_Window_(d, iTrue /* we were already redrawing during the resize */);

         return iTrue;

     case SDL_WINDOWEVENT_LEAVE:

         unhover_Widget();

@@ -881,6 +1528,7 @@ static iBool handleWindowEvent_Window_(iWindow *d, const SDL_WindowEvent *ev) {

         return iTrue;

     case SDL_WINDOWEVENT_TAKE_FOCUS:

         SDL_SetWindowInputFocus(d->win);

         return iTrue;

     case SDL_WINDOWEVENT_FOCUS_GAINED:

         d->focusGainedAt = SDL_GetTicks();

@@ -904,13 +1552,22 @@ static void applyCursor_Window_(iWindow *d) {

iBool processEvent_Window(iWindow *d, const SDL_Event *ev) {

 switch (ev->type) {

+#if defined (LAGRANGE_CUSTOM_FRAME)

+#endif

     case SDL_WINDOWEVENT: {

         return handleWindowEvent_Window_(d, &ev->window);

     }

     case SDL_RENDER_TARGETS_RESET:

     case SDL_RENDER_DEVICE_RESET: {

         break;

     }

     default: {

@@ -927,12 +1584,19 @@ iBool processEvent_Window(iWindow *d, const SDL_Event *ev) {

             postRefresh_App();

             return iTrue;

         }

         if (event.type == SDL_KEYDOWN && SDL_GetTicks() - d->focusGainedAt < 10) {

             /* Suspiciously close to when input focus was received. For example under openbox,

                closing xterm with Ctrl+D will cause the keydown event to "spill" over to us.

                As a workaround, ignore these events. */

             return iTrue; /* won't go to bindings, either */

         }

         /* Map mouse pointer coordinate to our coordinate system. */

         if (event.type == SDL_MOUSEMOTION) {

             setCursor_Window(d, SDL_SYSTEM_CURSOR_ARROW); /* default cursor */

@@ -985,15 +1649,42 @@ void draw_Window(iWindow *d) {

 if (d->isDrawFrozen) {

     return;

 }

-//#if !defined (NDEBUG)

-// printf("draw %d\n", d->frameTime); fflush(stdout);

-//#endif

+#if defined (iPlatformAppleMobile)

+#else

+#endif

 /* Draw widgets. */

 d->frameTime = SDL_GetTicks();

 draw_Widget(d->root);

+#if defined (LAGRANGE_CUSTOM_FRAME)

+#endif

#if 0

 /* Text cache debugging. */ {

     SDL_Texture *cache = glyphCache_Text();

@@ -1013,6 +1704,10 @@ void resize_Window(iWindow *d, int w, int h) {

void setTitle_Window(iWindow *d, const iString *title) {

 SDL_SetWindowTitle(d->win, cstr_String(title));

}

void setUiScale_Window(iWindow *d, float uiScale) {

@@ -1048,7 +1743,7 @@ uint32_t id_Window(const iWindow *d) {

}

iInt2 rootSize_Window(const iWindow *d) {

}

iInt2 coord_Window(const iWindow *d, int x, int y) {

@@ -1081,3 +1776,104 @@ uint32_t frameTime_Window(const iWindow *d) {

iWindow *get_Window(void) {

 return theWindow_;

}

+void setSnap_Window(iWindow *d, int snapMode) {

+#if defined (LAGRANGE_CUSTOM_FRAME)

+#endif /* defined (LAGRANGE_CUSTOM_FRAME) */

+}

+int snap_Window(const iWindow *d) {

+}

diff --git a/src/ui/window.h b/src/ui/window.h

index 8be8b88d..7cd29d4b 100644

--- a/src/ui/window.h

+++ b/src/ui/window.h

@@ -32,13 +32,38 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */

iDeclareType(Window)

iDeclareTypeConstructionArgs(Window, iRect rect)

+enum iWindowSnap {

+};

+iDeclareType(WindowPlacement)

+/* Tracking of window placement. */

+struct Impl_WindowPlacement {

+};

struct Impl_Window {

 SDL_Window *  win;

 iBool         isDrawFrozen; /* avoids premature draws while restoring window state */

 iBool         isMouseInside;

 uint32_t      focusGainedAt;

 SDL_Renderer *render;

 iWidget *     root;

@@ -46,6 +71,7 @@ struct Impl_Window {

 float         uiScale;

 uint32_t      frameTime;

 double        presentTime;

 SDL_Cursor *  cursors[SDL_NUM_SYSTEM_CURSORS];

 SDL_Cursor *  pendingCursor;

 int           loadAnimTimer;

@@ -59,6 +85,7 @@ void setTitle_Window (iWindow *, const iString *title);

void setUiScale_Window (iWindow *, float uiScale);

void setFreezeDraw_Window (iWindow *, iBool freezeDraw);

void setCursor_Window (iWindow *, int cursor);

+void setSnap_Window (iWindow *, int snapMode);

uint32_t id_Window (const iWindow *);

iInt2 rootSize_Window (const iWindow *);

@@ -67,5 +94,11 @@ iInt2 coord_Window (const iWindow *, int x, int y);

iInt2 mouseCoord_Window (const iWindow *);

uint32_t frameTime_Window (const iWindow *);

SDL_Renderer *renderer_Window (const iWindow *);

+int snap_Window (const iWindow *);

+iBool isFullscreen_Window (const iWindow *);

iWindow * get_Window (void);

+#if defined (LAGRANGE_CUSTOM_FRAME)

+SDL_HitTestResult hitTest_Window(const iWindow *d, iInt2 pos);

+#endif

diff --git a/src/win32.c b/src/win32.c

index 5eb2a0d5..01ec73bf 100644

--- a/src/win32.c

+++ b/src/win32.c

@@ -21,10 +21,14 @@ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT

SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */

#include "win32.h"

+#include "ui/window.h"

+#include "app.h"

#include <SDL_syswm.h>

#define WIN32_LEAN_AND_MEAN

#include <Windows.h>

+#include <windowsx.h>

+#include <dwmapi.h>

#include <d2d1.h>

void setDPIAware_Win32(void) {

@@ -59,3 +63,132 @@ void useExecutableIconResource_SDLWindow(SDL_Window *win) {

     }

 }

}

+#if defined (LAGRANGE_CUSTOM_FRAME)

+iInt2 cursor_Win32(void) {

+}

+void processNativeEvent_Win32(const struct SDL_SysWMmsg *msg, iWindow *window) {

+#if 0

+#endif

+#if 0

+#endif

+}

+#endif /* defined (LAGRANGE_CUSTOM_FRAME) */

diff --git a/src/win32.h b/src/win32.h

index b5bc0b45..27aa0539 100644

--- a/src/win32.h

+++ b/src/win32.h

@@ -21,8 +21,19 @@ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT

SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */

#pragma once

+#include <the_Foundation/vec2.h>

#include <SDL_video.h>

+#include <SDL_syswm.h>

+iDeclareType(Window)

void setDPIAware_Win32(void);

float desktopDPI_Win32(void);

void useExecutableIconResource_SDLWindow(SDL_Window *win);

+#if defined (LAGRANGE_CUSTOM_FRAME)

+iInt2 cursor_Win32(void);

+void processNativeEvent_Win32(const struct SDL_SysWMmsg *msg, iWindow *window);

+void setup_SDLWindow(SDL_Window *);

+#endif

--

2.25.1

Proxy Information
Original URL
gemini://git.skyjake.fi/lagrange/work%2Fv1.10/patch/b35539d9181bbef06da4fe7f61e55cad822df756.patch
Status Code
Success (20)
Meta
text/plain
Capsule Response Time
289.883954 milliseconds
Gemini-to-HTML Time
139.195762 milliseconds

This content has been proxied by September (ba2dc).