diff --git a/res/arabic.fontpack/fontpack.ini b/res/arabic.fontpack/fontpack.ini

index 00ad5241..305878ce 100644

--- a/res/arabic.fontpack/fontpack.ini

+++ b/res/arabic.fontpack/fontpack.ini

@@ -1,3 +1,5 @@

+version = 1

+

[arabic]

name = "Noto Sans Arabic UI"

auxiliary = true

diff --git a/res/cjk.fontpack/fontpack.ini b/res/cjk.fontpack/fontpack.ini

index fbac54be..6e0d274c 100644

--- a/res/cjk.fontpack/fontpack.ini

+++ b/res/cjk.fontpack/fontpack.ini

@@ -1,3 +1,5 @@

+version = 1

+

[notosansjp]

name = "Noto Sans JP"

auxiliary = true

diff --git a/res/default.fontpack/fontpack.ini b/res/default.fontpack/fontpack.ini

index 68316ef6..f8ef31ce 100644

--- a/res/default.fontpack/fontpack.ini

+++ b/res/default.fontpack/fontpack.ini

@@ -17,6 +17,8 @@

glyphscale and voffset can also be specified separately for the UI and

document domains by prefixing ui. or doc. to the key.



+version = 1

+

[default]

name = "Source Sans"

regular = "SourceSans3-Regular.ttf"

diff --git a/res/firasans.fontpack/fontpack.ini b/res/firasans.fontpack/fontpack.ini

index 4378a757..c6eb4c77 100644

--- a/res/firasans.fontpack/fontpack.ini

+++ b/res/firasans.fontpack/fontpack.ini

@@ -1,3 +1,5 @@

+version = 1

+

[firasans]

name = "Fira Sans"

glyphscale = 0.85

diff --git a/res/literata.fontpack/fontpack.ini b/res/literata.fontpack/fontpack.ini

index e4e49bcb..7c29491d 100644

--- a/res/literata.fontpack/fontpack.ini

+++ b/res/literata.fontpack/fontpack.ini

@@ -1,3 +1,5 @@

+version = 1

+

[literata]

name = "Literata"

regular = "Literata-Regular-opsz=14.ttf"

diff --git a/res/nunito.fontpack/fontpack.ini b/res/nunito.fontpack/fontpack.ini

index ea4a12b8..2f2471e1 100644

--- a/res/nunito.fontpack/fontpack.ini

+++ b/res/nunito.fontpack/fontpack.ini

@@ -1,3 +1,5 @@

+version = 1

+

[nunito]

name = "Nunito"

tweaks = 0x1 # some hardcoded kerning changes (Th, etc.)

diff --git a/res/tinos.fontpack/fontpack.ini b/res/tinos.fontpack/fontpack.ini

index 8759b752..a2cf811e 100644

--- a/res/tinos.fontpack/fontpack.ini

+++ b/res/tinos.fontpack/fontpack.ini

@@ -1,3 +1,5 @@

+version = 1

+

[tinos]

name = "Tinos"

glyphscale = 0.850

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

index b317e7b3..cb5479e8 100644

--- a/src/app.c

+++ b/src/app.c

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

     const iBool isSplit = numRoots_Window(get_Window()) > 1;

     if (tabCount_Widget(tabs) > 1 || isSplit) {

         iWidget *closed = removeTabPage_Widget(tabs, index);

+ cancelAllRequests_DocumentWidget((iDocumentWidget *) closed);

         destroy_Widget(closed); /* released later */

         if (index == tabCount_Widget(tabs)) {

             index--;

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

index 65096389..f5479cf3 100644

--- a/src/defs.h

+++ b/src/defs.h

@@ -125,11 +125,13 @@ iLocalDef int acceptKeyMod_ReturnKeyBehavior(int behavior) {

#define delete_Icon "\u232b"

#define copy_Icon "\u2398" //"\u2bba"

#define check_Icon "\u2714"

-#define ballotCheck_Icon "\U0001f5f9"

+#define ballotChecked_Icon "\U0001f5f9"

+#define ballotUnchecked_Icon "\U0001f5f9"

#define inbox_Icon "\U0001f4e5"

#define book_Icon "\U0001f56e"

#define bookmark_Icon "\U0001f516"

#define folder_Icon "\U0001f4c1"

+#define file_Icon "\U0001f5ce"

#define openTab_Icon "\u2750"

#define openTabBg_Icon "\u2b1a"

#define openExt_Icon "\u27a0"

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

index ca1d1582..fb1c98ee 100644

--- a/src/fontpack.c

+++ b/src/fontpack.c

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

#include <the_Foundation/string.h>

#include <the_Foundation/toml.h>



-/* TODO: Clean up and/or reorder this file, it's a bit unorganized. */

+const char *mimeType_FontPack = "application/lagrange-fontpack+zip";



float scale_FontSize(enum iFontSize size) {

 static const float sizes[max_FontSize] = {

@@ -57,38 +57,9 @@ float scale_FontSize(enum iFontSize size) {

 return sizes[size];

}



-iDeclareType(Fonts) 

- 

-struct Impl_Fonts {

- iString userDir;

- iPtrArray packs;

- iPtrArray files;

- iPtrArray specOrder; /* specs sorted by priority */

-};

-

-static iFonts fonts_;

-

-static void unloadFiles_Fonts_(iFonts *d) {

- /* TODO: Mark all files in font packs as not resident. */ 

- iForEach(PtrArray, i, &d->files) {

- delete_FontFile(i.ptr);

- }

- clear_PtrArray(&d->files);

-}

-

-static iFontFile *findFile_Fonts_(iFonts *d, const iString *id) {

- iForEach(PtrArray, i, &d->files) {

- iFontFile *ff = i.ptr;

- if (equal_String(&ff->id, id)) {

- return ff;

- }

- }

- return NULL;

-}

-

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



-iDefineTypeConstruction(FontFile)

+iDefineObjectConstruction(FontFile)



void init_FontFile(iFontFile *d) {

 init_String(&d->id);

@@ -114,7 +85,7 @@ static void load_FontFile_(iFontFile *d, const iBlock *data) {

                            HB_MEMORY_MODE_READONLY, NULL, NULL);

 d->hbFace = hb_face_create(d->hbBlob, 0);

 d->hbFont = hb_font_create(d->hbFace);

-#endif 

+#endif

}



static void unload_FontFile_(iFontFile *d) {

@@ -126,12 +97,13 @@ static void unload_FontFile_(iFontFile *d) {

 d->hbFont = NULL;

 d->hbFace = NULL;

 d->hbBlob = NULL;

-#endif 

+#endif

 clear_Block(&d->sourceData);

 iZap(d->stbInfo);

}



void deinit_FontFile(iFontFile *d) {

+ printf("FontFile %p {%s} is DESTROYED\n", d, cstr_String(&d->id));

 unload_FontFile_(d);

 deinit_Block(&d->sourceData);

 deinit_String(&d->id);

@@ -156,12 +128,12 @@ void measureGlyph_FontFile(const iFontFile *d, uint32_t glyphIndex,



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



-

iDefineTypeConstruction(FontSpec)

 

void init_FontSpec(iFontSpec *d) {

 init_String(&d->id);

 init_String(&d->name);

+ init_String(&d->sourcePath);

 d->flags      = 0;

 d->priority   = 0;

 for (int i = 0; i < 2; ++i) {

@@ -173,52 +145,119 @@ void init_FontSpec(iFontSpec *d) {

}



void deinit_FontSpec(iFontSpec *d) {

+ /* FontFile references are held by FontSpecs. */

+ iForIndices(i, d->styles) {

+ iRelease(d->styles[i]);

+ }

+ deinit_String(&d->sourcePath);

 deinit_String(&d->name);

 deinit_String(&d->id);

}



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



-iDeclareType(FontPack)

-iDeclareTypeConstruction(FontPack)

+iDeclareType(Fonts)

+

+struct Impl_Fonts {

+ iString userDir;

+ iPtrArray packs;

+ iObjectList *files;

+ iPtrArray specOrder; /* specs sorted by priority */

+};

+

+static iFonts fonts_;

+

+static void unloadFiles_Fonts_(iFonts *d) {

+ /* TODO: Mark all files in font packs as not resident. */ 

+ clear_ObjectList(d->files);

+}

+

+static iFontFile *findFile_Fonts_(iFonts *d, const iString *id) {

+ iForEach(ObjectList, i, d->files) {

+ iFontFile *ff = i.object;

+ if (equal_String(&ff->id, id)) {

+ return ff;

+ }

+ }

+ return NULL;

+}

+

+static void releaseUnusedFiles_Fonts_(iFonts *d) {

+ iForEach(ObjectList, i, d->files) {

+ iFontFile *ff = i.object;

+ if (ff->object.refCount == 1) {

+ /* No specs use this. */

+ //printf("[Fonts] releasing unused font file: %p {%s}\n", ff, cstr_String(&ff->id));

+ remove_ObjectListIterator(&i);

+ }

+ }

+}

+

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



struct Impl_FontPack {

- const iArchive archive; / opened ZIP archive */

+ iString id; /* lowercase filename without the .fontpack extension */

+ int version;

+ iBool isStandalone;

+ iBool isReadOnly;

 iArray          fonts;   /* array of FontSpecs */

+ const iArchive archive; / opened ZIP archive */

 iString *       loadPath;

 iFontSpec *     loadSpec;

};



+iDefineTypeConstruction(FontPack)

+

void init_FontPack(iFontPack *d) {

- d->archive = NULL;

+ init_String(&d->id);

+ d->version = 0;

+ d->isStandalone = iFalse;

+ d->isReadOnly = iFalse;

 init_Array(&d->fonts, sizeof(iFontSpec));

+ d->archive = NULL;

 d->loadSpec = NULL;

 d->loadPath = NULL;

}



void deinit_FontPack(iFontPack *d) {

+ iAssert(d->archive == NULL);

+ iAssert(d->loadSpec == NULL);

 delete_String(d->loadPath);

 iForEach(Array, i, &d->fonts) {

     deinit_FontSpec(i.value);

 }

 deinit_Array(&d->fonts);

- iAssert(d->archive == NULL);

- iAssert(d->loadSpec == NULL);

+ deinit_String(&d->id);

+ releaseUnusedFiles_Fonts_(&fonts_);

}



-iDefineTypeConstruction(FontPack)

+iFontPackId id_FontPack(const iFontPack *d) {

+ return (iFontPackId){ &d->id, d->version };

+}

+

+const iPtrArray *listSpecs_FontPack(const iFontPack *d) {

+ if (!d) return NULL;

+ iPtrArray *list = collectNew_PtrArray();

+ iConstForEach(Array, i, &d->fonts) {

+ pushBack_PtrArray(list, i.value);

+ }

+ return list;

+}



void handleIniTable_FontPack_(void *context, const iString *table, iBool isStart) {

 iFontPack *d = context;

 if (isStart) {

     iAssert(!d->loadSpec);

- /* Each font ID must be unique. */

- if (!findSpec_Fonts(cstr_String(table))) {

+ /* Each font ID must be unique in the non-standalone packs. */

+ if (d->isStandalone || !findSpec_Fonts(cstr_String(table))) {

         d->loadSpec = new_FontSpec();

         set_String(&d->loadSpec->id, table);

+ if (d->loadPath) {

+ set_String(&d->loadSpec->sourcePath, d->loadPath);

+ }

     }

 }

- else {

+ else if (d->loadSpec) {

     /* Set fallback font files. */ {

         const iFontFile **styles = d->loadSpec->styles;

         if (!styles[regular_FontStyle]) {

@@ -229,11 +268,11 @@ void handleIniTable_FontPack_(void *context, const iString *table, iBool isStart

             return;

         }

         if (!styles[semiBold_FontStyle]) {

- styles[semiBold_FontStyle] = styles[bold_FontStyle];

+ styles[semiBold_FontStyle] = ref_Object(styles[bold_FontStyle]);

         }

         for (size_t s = 0; s < max_FontStyle; s++) {

             if (!styles[s]) {

- styles[s] = styles[regular_FontStyle];

+ styles[s] = ref_Object(styles[regular_FontStyle]);

             }

         }

     }

@@ -263,7 +302,15 @@ static iBlock *readFile_FontPack_(const iFontPack *d, const iString *path) {

void handleIniKeyValue_FontPack_(void *context, const iString *table, const iString *key,

                              const iTomlValue *value) {

 iFontPack *d = context;

- if (!d->loadSpec) return;

+ if (isEmpty_String(table)) {

+ if (!cmp_String(key, "version")) {

+ d->version = number_TomlValue(value);

+ }

+ return;

+ }

+ if (!d->loadSpec) {

+ return;

+ }

 iUnused(table);

 if (!cmp_String(key, "name") && value->type == string_TomlType) {

     set_String(&d->loadSpec->name, value->value.string);        

@@ -322,12 +369,13 @@ void handleIniKeyValue_FontPack_(void *context, const iString *table, const iStr

                     ff = new_FontFile();

                     set_String(&ff->id, fontFileId);

                     load_FontFile_(ff, data);

- pushBack_PtrArray(&fonts_.files, ff); /* centralized ownership */

+ pushBack_ObjectList(fonts_.files, ff); /* centralized ownership */

+ iRelease(ff);

                     delete_Block(data);

// printf("[FontPack] loaded file: %s\n", cstr_String(fontFileId));

                 }

             }

- d->loadSpec->styles[i] = ff;

+ d->loadSpec->styles[i] = ref_Object(ff);

             delete_String(fontFileId);

             break;

         }

@@ -348,29 +396,6 @@ static iBool load_FontPack_(iFontPack *d, const iString *ini) {

 return ok;

}



-#if 0

-iBool loadIniFile_FontPack(iFontPack *d, const iString *iniPath) {

- iBeginCollect();

- iBool ok = iFalse;

- iFile *f = iClob(new_File(iniPath));

- if (open_File(f, text_FileMode | readOnly_FileMode)) {

- d->loadPath = collect_String(newRange_String(dirName_Path(iniPath)));

- iString *src = collect_String(readString_File(f));

- 

- iTomlParser *ini = collect_TomlParser(new_TomlParser());

- setHandlers_TomlParser(ini, handleIniTable_FontPack_, handleIniKeyValue_FontPack_, d);

- if (!parse_TomlParser(ini, src)) {

- fprintf(stderr, "[FontPack] error parsing %s\n", cstr_String(iniPath));

- }

- iAssert(d->loadSpec == NULL);

- d->loadPath = NULL;

- ok = iTrue;

- }

- iEndCollect();

- return ok;

-}

-#endif

-

iBool loadArchive_FontPack(iFontPack *d, const iArchive *zip) {

 d->archive = zip;

 iBool ok = iFalse;

@@ -387,6 +412,28 @@ iBool loadArchive_FontPack(iFontPack *d, const iArchive *zip) {

 return ok;

}



+void setLoadPath_FontPack(iFontPack *d, const iString *path) {

+ if (!d->loadPath) {

+ d->loadPath = new_String();

+ }

+ set_String(d->loadPath, path);

+ /* Pack ID is based on the file name. */

+ setRange_String(&d->id, baseName_Path(path));

+ setRange_String(&d->id, withoutExtension_Path(&d->id));

+}

+

+void setStandalone_FontPack(iFontPack *d, iBool standalone) {

+ d->isStandalone = standalone;

+}

+

+void setReadOnly_FontPack(iFontPack *d, iBool readOnly) {

+ d->isReadOnly = readOnly;

+}

+

+iBool isReadOnly_FontPack(const iFontPack *d) {

+ return d->isReadOnly;

+}

+

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



static void unloadFonts_Fonts_(iFonts *d) {

@@ -397,14 +444,23 @@ static void unloadFonts_Fonts_(iFonts *d) {

 clear_PtrArray(&d->packs);

}



+static int cmpName_FontSpecPtr_(const void *a, const void *b) {

+ const iFontSpec **p1 = (const iFontSpec **) a, **p2 = (const iFontSpec **) b;

+ return cmpStringCase_String(&(*p1)->name, &(*p2)->name);

+}

+

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

 const iFontSpec **p1 = (const iFontSpec **) a, **p2 = (const iFontSpec **) b;

- return -iCmp((*p1)->priority, (p2)->priority); / highest priority first */

+ const int cmp = -iCmp((*p1)->priority, (p2)->priority); / highest priority first */

+ if (cmp) return cmp;

+ return cmpName_FontSpecPtr_(a, b);

}



-static int cmpName_FontSpecPtr_(const void *a, const void *b) {

+static int cmpSourceAndPriority_FontSpecPtr_(const void *a, const void *b) {

 const iFontSpec **p1 = (const iFontSpec **) a, **p2 = (const iFontSpec **) b;

- return cmpStringCase_String(&(*p1)->name, &(*p2)->name);

+ const int cmp = cmpStringCase_String(&(*p1)->sourcePath, &(*p2)->sourcePath);

+ if (cmp) return cmp;

+ return cmpPriority_FontSpecPtr_(a, b);

}



static void sortSpecs_Fonts_(iFonts *d) {

@@ -422,11 +478,13 @@ void init_Fonts(const char *userDir) {

 iFonts *d = &fonts_;

 initCStr_String(&d->userDir, userDir);

 init_PtrArray(&d->packs);

- init_PtrArray(&d->files);

+ d->files = new_ObjectList();

 init_PtrArray(&d->specOrder);

 /* Load the required fonts. */ {

     iFontPack *pack = new_FontPack();

+ setCStr_String(&pack->id, "default");

     iArchive *arch = new_Archive();

+ setReadOnly_FontPack(pack, iTrue);

     openData_Archive(arch, &fontpackDefault_Embedded);

     loadArchive_FontPack(pack, arch); /* should never fail if we've made it this far */

     iRelease(arch);

@@ -436,7 +494,7 @@ void init_Fonts(const char *userDir) {

     const char *locations[] = {

         ".",

         "./fonts",

- "../share/lagrange",

+ "../share/lagrange", /* Note: These must match CMakeLists.txt install destination */

         "../../share/lagrange",

         concatPath_CStr(userDir, "fonts"),

         userDir,

@@ -450,7 +508,13 @@ void init_Fonts(const char *userDir) {

                 iArchive *arch = new_Archive();

                 if (openFile_Archive(arch, entryPath)) {

                     iFontPack *pack = new_FontPack();

- pack->loadPath = copy_String(entryPath);

+ setLoadPath_FontPack(pack, entryPath);

+ setReadOnly_FontPack(pack, !isWritable_FileInfo(entry.value));

+#if defined (iPlatformApple)

+ if (startsWith_String(pack->loadPath, cstr_String(execDir))) {

+ setReadOnly_FontPack(pack, iTrue);

+ }

+#endif

                     if (loadArchive_FontPack(pack, arch)) {

                         pushBack_PtrArray(&d->packs, pack);

                     }

@@ -491,13 +555,18 @@ void init_Fonts(const char *userDir) {

void deinit_Fonts(void) {

 iFonts *d = &fonts_;

 unloadFonts_Fonts_(d);

- unloadFiles_Fonts_(d);

+ //unloadFiles_Fonts_(d);

+ iAssert(isEmpty_ObjectList(d->files));

 deinit_PtrArray(&d->specOrder);

 deinit_PtrArray(&d->packs);

- deinit_PtrArray(&d->files);

+ iRelease(d->files);

 deinit_String(&d->userDir);

}



+const iPtrArray *listPacks_Fonts(void) {

+ return &fonts_.packs;

+}

+

const iFontSpec *findSpec_Fonts(const char *fontId) {

 iFonts *d = &fonts_;

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

@@ -524,3 +593,73 @@ const iPtrArray *listSpecs_Fonts(iBool (*filterFunc)(const iFontSpec *)) {

const iPtrArray *listSpecsByPriority_Fonts(void) {

 return &fonts_.specOrder;

}

+

+const iString *infoPage_Fonts(void) {

+ iFonts *d = &fonts_;

+ iString *str = collectNewCStr_String("# Fonts\n");

+ iPtrArray *specsByPack = collectNew_PtrArray();

+ setCopy_PtrArray(specsByPack, &d->specOrder);

+ sort_Array(specsByPack, cmpSourceAndPriority_FontSpecPtr_);

+ iString *currentSourcePath = collectNew_String();

+ iConstForEach(PtrArray, i, specsByPack) {

+ const iFontSpec *spec = i.ptr;

+ if (isEmpty_String(&spec->sourcePath)) {

+ continue; /* built-in font */

+ }

+ if (!equal_String(&spec->sourcePath, currentSourcePath)) {

+ appendFormat_String(str, "=> %s %s%s\n",

+ cstrCollect_String(makeFileUrl_String(&spec->sourcePath)),

+ endsWithCase_String(&spec->sourcePath, ".fontpack") ? "\U0001f520 " : "",

+ cstr_Rangecc(baseName_Path(&spec->sourcePath)));

+ set_String(currentSourcePath, &spec->sourcePath);

+ }

+ }

+ return str;

+}

+

+const iFontPack *findPack_Fonts(const iString *path) {

+ iFonts *d = &fonts_;

+ iConstForEach(PtrArray, i, &d->packs) {

+ const iFontPack *pack = i.ptr;

+ if (pack->loadPath && equal_String(pack->loadPath, path)) {

+ return pack;

+ }

+ }

+ return NULL;

+}

+

+iBool preloadLocalFontpackForPreview_Fonts(iGmDocument *doc) {

+ iBool wasLoaded = iFalse;

+ for (size_t linkId = 1; ; linkId++) {

+ const iString *linkUrl = linkUrl_GmDocument(doc, linkId);

+ if (!linkUrl) {

+ break; /* ran out of links */

+ }

+ const int linkFlags = linkFlags_GmDocument(doc, linkId);

+ if (linkFlags & fontpackFileExtension_GmLinkFlag &&

+ scheme_GmLinkFlag(linkFlags) == file_GmLinkScheme) {

+ iMediaId linkMedia = findMediaForLink_Media(media_GmDocument(doc), linkId, fontpack_MediaType);

+ if (linkMedia.type) {

+ continue; /* got this one already */

+ }

+ iString *filePath = localFilePathFromUrl_String(linkUrl);

+ iFile *f = new_File(filePath);

+ if (open_File(f, readOnly_FileMode)) {

+ iBlock *fontPackArchiveData = readAll_File(f);

+ setUrl_Media(media_GmDocument(doc), linkId, fontpack_MediaType, linkUrl);

+ setData_Media(media_GmDocument(doc),

+ linkId,

+ collectNewCStr_String(mimeType_FontPack),

+ fontPackArchiveData,

+ 0);

+ delete_Block(fontPackArchiveData);

+ wasLoaded = iTrue;

+ }

+ iRelease(f);

+ }

+ }

+ return wasLoaded;

+}

+

+iDefineClass(FontFile)

+

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

index e59154e3..429afb5d 100644

--- a/src/fontpack.h

+++ b/src/fontpack.h

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

include <hb.h>

#endif



+extern const char *mimeType_FontPack;

+

/* Fontpacks are ZIP archives that contain a configuration file and one of more font

files. The fontpack format is used instead of plain TTF/OTF because the text renderer

uses additional metadata about each font.

@@ -68,21 +70,13 @@ enum iFontStyle {



float scale_FontSize (enum iFontSize size);



-iDeclareType(FontSpec)

-iDeclareTypeConstruction(FontSpec)

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



-enum iFontSpecFlags {

- override_FontSpecFlag = iBit(1),

- monospace_FontSpecFlag = iBit(2), /* can be used in preformatted content */

- auxiliary_FontSpecFlag = iBit(3), /* only used for looking up glyphs missing from other fonts */

- arabic_FontSpecFlag = iBit(4),

- fixNunitoKerning_FontSpecFlag = iBit(31), /* manual hardcoded kerning tweaks for Nunito */

-};

-

-iDeclareType(FontFile)

-iDeclareTypeConstruction(FontFile)

+iDeclareClass(FontFile)

+iDeclareObjectConstruction(FontFile)

 

struct Impl_FontFile {

+ iObject object; /* reference-counted */

 iString         id; /* for detecting when the same file is used in many places */

 enum iFontStyle style;

 iBlock          sourceData;

@@ -107,9 +101,26 @@ uint8_t * rasterizeGlyph_FontFile(const iFontFile *, float xScale, float yScal

void measureGlyph_FontFile (const iFontFile *, uint32_t glyphIndex,

                                 float xScale, float yScale, float xShift,

                                 int *x0, int *y0, int *x1, int *y1);

+

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

+

+/* FontSpec describes a typeface, combining multiple fonts into a group.

+ The user will be choosing FontSpecs instead of individual font files. */

+iDeclareType(FontSpec)

+iDeclareTypeConstruction(FontSpec)

+

+enum iFontSpecFlags {

+ override_FontSpecFlag = iBit(1),

+ monospace_FontSpecFlag = iBit(2), /* can be used in preformatted content */

+ auxiliary_FontSpecFlag = iBit(3), /* only used for looking up glyphs missing from other fonts */

+ arabic_FontSpecFlag = iBit(4),

+ fixNunitoKerning_FontSpecFlag = iBit(31), /* manual hardcoded kerning tweaks for Nunito */

+};

+

struct Impl_FontSpec {

 iString id;   /* unique ID */

 iString name; /* human-readable label */

+ iString sourcePath; /* file where the path was loaded, could be a .fontpack */

 int     flags;

 int     priority;

 float   heightScale[2];     /* overall height scaling; ui, document */

@@ -121,10 +132,38 @@ struct Impl_FontSpec {

iLocalDef int scaleType_FontSpec(enum iFontSize sizeId) {

 return sizeId / contentRegular_FontSize;

}

- 

+

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

+

+iDeclareType(FontPack)

+iDeclareTypeConstruction(FontPack)

+

+iDeclareType(FontPackId)

+

+struct Impl_FontPackId {

+ const iString *id;

+ int version;

+};

+

+void setReadOnly_FontPack (iFontPack *, iBool readOnly);

+void setStandalone_FontPack (iFontPack *, iBool standalone);

+void setLoadPath_FontPack (iFontPack *, const iString *path);

+iBool loadArchive_FontPack (iFontPack *, const iArchive *zip);

+

+iFontPackId id_FontPack (const iFontPack *);

+const iPtrArray * listSpecs_FontPack (const iFontPack *);

+iBool isReadOnly_FontPack (const iFontPack *);

+

+iDeclareType(GmDocument)

+

void init_Fonts (const char *userDir);

void deinit_Fonts (void);



+const iFontPack * findPack_Fonts (const iString *path);

const iFontSpec * findSpec_Fonts (const char *fontId);

+const iPtrArray * listPacks_Fonts (void);

const iPtrArray * listSpecs_Fonts (iBool (*filterFunc)(const iFontSpec *));

const iPtrArray * listSpecsByPriority_Fonts (void);

+const iString * infoPage_Fonts (void);

+

+iBool preloadLocalFontpackForPreview_Fonts (iGmDocument *doc);

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

index 23846414..952d72a1 100644

--- a/src/gempub.c

+++ b/src/gempub.c

@@ -337,7 +337,7 @@ iBool preloadCoverImage_Gempub(const iGempub *d, iGmDocument *doc) {

 for (size_t linkId = 1; ; linkId++) {

     const iString *linkUrl = linkUrl_GmDocument(doc, linkId);

     if (!linkUrl) break;

- if (findLinkImage_Media(media_GmDocument(doc), linkId)) {

+ if (findLinkImage_Media(media_GmDocument(doc), linkId).type) {

         continue; /* got this already */

     }

     if (linkFlags_GmDocument(doc, linkId) & imageFileExtension_GmLinkFlag) {

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

index c98b0bb8..c271ad94 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/color.h"

#include "ui/text.h"

#include "ui/metrics.h"

+#include "ui/mediaui.h"

#include "ui/window.h"

#include "visited.h"

#include "bookmarks.h"

@@ -224,7 +225,7 @@ static iRangecc addLink_GmDocument_(iGmDocument *d, iRangecc line, iGmLinkId *li

             setScheme_GmLink_(link, finger_GmLinkScheme);

         }

         else if (equalCase_Rangecc(parts.scheme, "file")) {

- setScheme_GmLink_(link, file_GmLinkScheme);

+ setScheme_GmLink_(link, file_GmLinkScheme); 

         }

         else if (equalCase_Rangecc(parts.scheme, "data")) {

             setScheme_GmLink_(link, data_GmLinkScheme);

@@ -251,6 +252,9 @@ static iRangecc addLink_GmDocument_(iGmDocument *d, iRangecc line, iGmLinkId *li

                      endsWithCase_String(path, ".mid") || endsWithCase_String(path, ".ogg")) {

                 link->flags |= audioFileExtension_GmLinkFlag;

             }

+ else if (endsWithCase_String(path, ".fontpack")) {

+ link->flags |= fontpackFileExtension_GmLinkFlag;

+ }

             delete_String(path);

         }

         /* Check if visited. */

@@ -503,7 +507,7 @@ static void doLayout_GmDocument_(iGmDocument *d) {

 static const char *arrow           = rightArrowhead_Icon;

 static const char *envelope        = envelope_Icon;

 static const char *bullet          = "\u2022";

- static const char *folder = "\U0001f4c1";

+ static const char *folder = file_Icon;

 static const char *globe           = globe_Icon;

 static const char *quote           = "\u201c";

 static const char *magnifyingGlass = "\U0001f50d";

@@ -900,77 +904,77 @@ static void doLayout_GmDocument_(iGmDocument *d) {

     ((iGmRun *) back_Array(&d->layout))->flags |= endOfLine_GmRunFlag;

     /* Image or audio content. */

     if (type == link_GmLineType) {

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

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

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

- if (imageId) {

- iGmMediaInfo img;

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

- const iInt2 imgSize = imageSize_Media(d->media, imageId);

- linkContentWasLaidOut_GmDocument_(d, &img, run.linkId);

- const int margin = lineHeight_Text(paragraph_FontId) / 2;

+ /* TODO: Cleanup here? Move to a function of its own. */

+// enum iMediaType mediaType = none_MediaType;

+ const iMediaId media = findMediaForLink_Media(d->media, run.linkId, none_MediaType);

+ iGmMediaInfo info;

+ info_Media(d->media, media, &info);

+ run.mediaType = media.type;

+ run.mediaId = media.id;

+ run.text = iNullRange;

+ run.font = uiLabel_FontId;

+ run.color = 0;

+ const int margin = lineHeight_Text(paragraph_FontId) / 2;

+ if (media.type) {

             pos.y += margin;

- run.bounds.pos = pos;

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

- const float aspect = (float) imgSize.y / (float) imgSize.x;

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

- /* Extend the image to full width, including outside margin, if the viewport

- is narrow enough. */

- if (isFullWidthImages) {

- run.bounds.size.x += d->outsideMargin * 2;

- run.bounds.size.y += d->outsideMargin * 2 * aspect;

- run.bounds.pos.x -= d->outsideMargin;

- } 

- run.visBounds = run.bounds;

- const iInt2 maxSize = mulf_I2(imgSize, get_Window()->pixelRatio);

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

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

- run.visBounds.size.y =

- run.visBounds.size.y * maxSize.x / width_Rect(run.visBounds);

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

- run.visBounds.pos.x = run.bounds.size.x / 2 - width_Rect(run.visBounds) / 2;

- run.bounds.size.y = run.visBounds.size.y;

- }

- run.text = iNullRange;

- run.font = 0;

- run.color = 0;

- run.mediaType = image_GmRunMediaType;

- run.mediaId = imageId;

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

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

- }

- else if (audioId) {

- iGmMediaInfo info;

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

+ run.bounds.size.y = 0;

             linkContentWasLaidOut_GmDocument_(d, &info, run.linkId);

- 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 = lineHeight_Text(uiContent_FontId) + 3 * gap_UI;

- run.visBounds = run.bounds;

- run.text = iNullRange;

- run.color = 0;

- run.mediaType = audio_GmRunMediaType;

- run.mediaId = audioId;

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

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

         }

- else if (downloadId) {

- iGmMediaInfo info;

- downloadInfo_Media(d->media, downloadId, &info);

- linkContentWasLaidOut_GmDocument_(d, &info, run.linkId);

- 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 = 2 * lineHeight_Text(uiContent_FontId) + 4 * gap_UI;

- run.visBounds = run.bounds;

- run.text = iNullRange;

- run.color = 0;

- run.mediaType = download_GmRunMediaType;

- run.mediaId = downloadId;

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

+ switch (media.type) {

+ case image_MediaType: {

+ const iInt2 imgSize = imageSize_Media(d->media, media);

+ run.bounds.pos = pos;

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

+ const float aspect = (float) imgSize.y / (float) imgSize.x;

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

+ /* Extend the image to full width, including outside margin, if the viewport

+ is narrow enough. */

+ if (isFullWidthImages) {

+ run.bounds.size.x += d->outsideMargin * 2;

+ run.bounds.size.y += d->outsideMargin * 2 * aspect;

+ run.bounds.pos.x -= d->outsideMargin;

+ }

+ run.visBounds = run.bounds;

+ const iInt2 maxSize = mulf_I2(imgSize, get_Window()->pixelRatio);

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

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

+ run.visBounds.size.y =

+ run.visBounds.size.y * maxSize.x / width_Rect(run.visBounds);

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

+ run.visBounds.pos.x = run.bounds.size.x / 2 - width_Rect(run.visBounds) / 2;

+ run.bounds.size.y = run.visBounds.size.y;

+ }

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

+ break;

+ }

+ case audio_MediaType: {

+ run.bounds.pos = pos;

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

+ run.bounds.size.y = lineHeight_Text(uiContent_FontId) + 3 * gap_UI;

+ run.visBounds = run.bounds;

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

+ break;

+ }

+ case download_MediaType: {

+ run.bounds.pos = pos;

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

+ run.bounds.size.y = 2 * lineHeight_Text(uiContent_FontId) + 4 * gap_UI;

+ run.visBounds = run.bounds;

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

+ break;

+ }

+ case fontpack_MediaType: {

+ run.bounds.pos = pos;

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

+ run.bounds.size.y = height_FontpackUI(d->media, media.id, d->size.x);

+ run.visBounds = run.bounds;

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

+ break;

+ }

+ default:

+ break;

+ }

+ if (media.type && run.bounds.size.y) {

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

         }

     }

@@ -1057,21 +1061,6 @@ const iString *url_GmDocument(const iGmDocument *d) {

 return &d->url;

}



-#if 0

-void reset_GmDocument(iGmDocument *d) {

- clear_Media(d->media);

- clearLinks_GmDocument_(d);

- clear_Array(&d->layout);

- clear_Array(&d->headings);

- clear_Array(&d->preMeta);

- clear_String(&d->url);

- clear_String(&d->localHost);

- clear_String(&d->source);

- clear_String(&d->unormSource);

- d->themeSeed = 0;

-}

-#endif

-

static void setDerivedThemeColors_(enum iGmDocumentTheme theme) {

 set_Color(tmQuoteIcon_ColorId,

           mix_Color(get_Color(tmQuote_ColorId), get_Color(tmBackground_ColorId), 0.55f));

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

index b2c6d9b7..20bc9890 100644

--- a/src/gmdocument.h

+++ b/src/gmdocument.h

@@ -90,6 +90,7 @@ enum iGmLinkFlag {

 query_GmLinkFlag              = iBit(14), /* Gopher query link */

 iconFromLabel_GmLinkFlag      = iBit(15), /* use an Emoji/special character from label */

 isOpen_GmLinkFlag             = iBit(16), /* currently open in a tab */

+ fontpackFileExtension_GmLinkFlag = iBit(17),

};



iLocalDef enum iGmLinkScheme scheme_GmLinkFlag(int flags) {

@@ -126,13 +127,6 @@ enum iGmRunFlags {

 altText_GmRunFlag     = iBit(8),

};



-enum iGmRunMediaType {

- none_GmRunMediaType,

- image_GmRunMediaType,

- audio_GmRunMediaType,

- download_GmRunMediaType,

-};

-

/* This structure is tightly packed because GmDocuments are mostly composed of

a large number of GmRuns. */

struct Impl_GmRun {

@@ -146,12 +140,16 @@ struct Impl_GmRun {

     uint32_t color     : 7; /* see max_ColorId */



     uint32_t font      : 10;

- uint32_t mediaType : 2;

- uint32_t mediaId : 10; /* zero if not an image */

+ uint32_t mediaType : 3;

+ uint32_t mediaId : 9; /* zero if not an image */

     uint32_t preId     : 10; /* preformatted block ID (sequential); merge with mediaId? */

 };

};



+iLocalDef iMediaId mediaId_GmRun(const iGmRun *d) {

+ return (iMediaId){ .type = d->mediaType, .id = d->mediaId };

+}

+

iDeclareType(GmRunRange)



struct Impl_GmRunRange {

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

index 1a9e83a9..f7a22e0a 100644

--- a/src/gmrequest.c

+++ b/src/gmrequest.c

@@ -361,6 +361,9 @@ static const iBlock *aboutPageSource_(iRangecc path, iRangecc query) {

 if (equalCase_Rangecc(path, "debug")) {

     return utf8_String(debugInfo_App());

 }

+ if (equalCase_Rangecc(path, "fonts")) {

+ return utf8_String(infoPage_Fonts());

+ }

 if (equalCase_Rangecc(path, "feeds")) {

     return utf8_String(entryListPage_Feeds());

 }

@@ -710,8 +713,9 @@ void submit_GmRequest(iGmRequest *d) {

         sort_Array(sortedInfo, (int (*)(const void *, const void *)) cmp_FileInfoPtr_);

         iForEach(PtrArray, s, sortedInfo) {

             const iFileInfo *entry = s.ptr;

- appendFormat_String(page, "=> %s %s%s\n",

+ appendFormat_String(page, "=> %s %s%s%s\n",

                                 cstrCollect_String(makeFileUrl_String(path_FileInfo(entry))),

+ isDirectory_FileInfo(entry) ? folder_Icon " " : "",

                                 cstr_Rangecc(baseName_Path(path_FileInfo(entry))),

                                 isDirectory_FileInfo(entry) ? iPathSeparator : "");

             iRelease(entry);

@@ -808,9 +812,10 @@ void submit_GmRequest(iGmRequest *d) {

                         const iString *subPath = e.value;

                         iRangecc relSub = range_String(subPath);

                         relSub.start += size_String(entryPath);

- appendFormat_String(page, "=> %s/%s %s\n",

+ appendFormat_String(page, "=> %s/%s %s%s\n",

                                             cstr_String(&d->url),

                                             cstr_String(withSpacesEncoded_String(collectNewRange_String(relSub))),

+ endsWith_Rangecc(relSub, "/") ? folder_Icon " " : "",

                                             cstr_Rangecc(relSub));

                     }

                     resp->statusCode = success_GmStatusCode;

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

index 971747d4..5be7e198 100644

--- a/src/gmutil.c

+++ b/src/gmutil.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 "gmutil.h"

+#include "fontpack.h"



#include <the_Foundation/file.h>

#include <the_Foundation/fileinfo.h>

@@ -511,7 +512,8 @@ const iString *findContainerArchive_Path(const iString *path) {

 while (!isEmpty_String(path) && cmp_String(path, ".")) {

     iString *dir = newRange_String(dirName_Path(path));

     if (endsWithCase_String(dir, ".zip") ||

- endsWithCase_String(dir, ".gpub")) {

+ endsWithCase_String(dir, ".gpub") ||

+ endsWithCase_String(dir, ".fontpack")) {

         iEndCollect();

         return collect_String(dir);

     }

@@ -534,6 +536,9 @@ const char *mediaTypeFromFileExtension_String(const iString *d) {

 else if (endsWithCase_String(d, ".gpub")) {

     return "application/gpub+zip";

 }

+ else if (endsWithCase_String(d, ".fontpack")) {

+ return mimeType_FontPack;

+ }

 else if (endsWithCase_String(d, ".xml")) {

     return "text/xml";

 }

@@ -562,6 +567,7 @@ const char *mediaTypeFromFileExtension_String(const iString *d) {

     return "audio/midi";

 }

 else if (endsWithCase_String(d, ".txt") ||

+ endsWithCase_String(d, ".ini") ||

          endsWithCase_String(d, ".md") ||

          endsWithCase_String(d, ".c") ||

          endsWithCase_String(d, ".h") ||

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

index 26f0af4b..0ce2ac5c 100644

--- a/src/media.c

+++ b/src/media.c

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



#include <the_Foundation/file.h>

#include <the_Foundation/ptrarray.h>

+#include <the_Foundation/stringlist.h>

#include <SDL_hints.h>

#include <SDL_render.h>

#include <SDL_timer.h>

@@ -287,45 +288,112 @@ iDefineTypeConstruction(GmDownload)



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



+iDeclareType(GmFontpack)

+

+struct Impl_GmFontpack {

+ iGmMediaProps props;

+ iString packId;

+ iFontpackMediaInfo info;

+ /* TODO: Font preview images? */

+};

+

+void init_GmFontpack(iGmFontpack *d) {

+ init_GmMediaProps_(&d->props);

+ init_String(&d->packId);

+ iZap(d->info);

+ d->info.names = new_StringList();

+}

+

+void deinit_GmFontpack(iGmFontpack *d) {

+ iRelease(d->info.names);

+ deinit_String(&d->packId);

+ deinit_GmMediaProps_(&d->props);

+}

+

+static void loadData_GmFontpack_(iGmFontpack *d, const iBlock *data) {

+ const iString *loadPath = collect_String(localFilePathFromUrl_String(&d->props.url));

+ const iFontPack *pack = findPack_Fonts(loadPath);

+ d->info.isValid = d->info.isInstalled = pack != NULL;

+ d->info.isReadOnly = iFalse;

+ if (!pack) {

+ /* Let's load it now temporarily and see what's inside. */

+ iArchive *zip = new_Archive();

+ if (openData_Archive(zip, data)) {

+ iFontPack *fp = collect_FontPack(new_FontPack());

+ setLoadPath_FontPack(fp, loadPath);

+ setStandalone_FontPack(fp, iTrue);

+ if (loadArchive_FontPack(fp, zip)) {

+ d->info.isValid = iTrue;

+ pack = fp;

+ }

+ }

+ iRelease(zip);

+ }

+ if (pack) {

+ set_String(&d->packId, id_FontPack(pack).id);

+ d->info.packId.id = &d->packId; /* we own this String */

+ d->info.packId.version = id_FontPack(pack).version;

+ d->info.isReadOnly = isReadOnly_FontPack(pack);

+ }

+ iPtrSet *unique = new_PtrSet();

+ iConstForEach(PtrArray, i, listSpecs_FontPack(pack)) {

+ const iFontSpec *spec = i.ptr;

+ pushBack_StringList(d->info.names, &spec->name);

+ iForIndices(j, spec->styles) {

+ insert_PtrSet(unique, spec->styles[j]);

+ }

+ }

+ iConstForEach(PtrSet, j, unique) {

+ d->info.sizeInBytes += size_Block(&((const iFontFile *) *j.value)->sourceData);

+ }

+ delete_PtrSet(unique);

+}

+

+iDefineTypeConstruction(GmFontpack)

+

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

+

struct Impl_Media {

- iPtrArray images;

- iPtrArray audio;

- iPtrArray downloads;

+ iPtrArray items[max_MediaType];

+ /* TODO: Add a hash to quickly look up a link's media. */

};



iDefineTypeConstruction(Media)



void init_Media(iMedia *d) {

- init_PtrArray(&d->images);

- init_PtrArray(&d->audio);

- init_PtrArray(&d->downloads);

+ iForIndices(i, d->items) {

+ init_PtrArray(&d->items[i]);

+ }

}



void deinit_Media(iMedia *d) {

 clear_Media(d);

- deinit_PtrArray(&d->downloads);

- deinit_PtrArray(&d->audio);

- deinit_PtrArray(&d->images);

+ iForIndices(i, d->items) {

+ deinit_PtrArray(&d->items[i]);

+ }

}



void clear_Media(iMedia *d) {

- iForEach(PtrArray, i, &d->images) {

+ iForEach(PtrArray, i, &d->items[image_MediaType]) {

     deinit_GmImage(i.ptr);

 }

- clear_PtrArray(&d->images);

- iForEach(PtrArray, a, &d->audio) {

+ iForEach(PtrArray, a, &d->items[audio_MediaType]) {

     deinit_GmAudio(a.ptr);

 }

- clear_PtrArray(&d->audio);

- iForEach(PtrArray, n, &d->downloads) {

+ iForEach(PtrArray, n, &d->items[download_MediaType]) {

     deinit_GmDownload(n.ptr);

 }

- clear_PtrArray(&d->downloads);

+ iForEach(PtrArray, f, &d->items[fontpack_MediaType]) {

+ deinit_GmFontpack(f.ptr);

+ }

+ iForIndices(type, d->items) {

+ clear_PtrArray(&d->items[type]);

+ }

}



size_t memorySize_Media(const iMedia *d) {

 size_t memSize = 0;

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

+ iConstForEach(PtrArray, i, &d->items[image_MediaType]) {

     const iGmImage *img = i.ptr;

     if (img->texture) {

         const iInt2 texSize = size_SDLTexture(img->texture);

@@ -335,34 +403,49 @@ size_t memorySize_Media(const iMedia *d) {

         memSize += size_Block(&img->partialData);

     }

 }

- iConstForEach(PtrArray, a, &d->audio) {

+ iConstForEach(PtrArray, a, &d->items[audio_MediaType]) {

     const iGmAudio *audio = a.ptr;

     if (audio->player) {

         memSize += sourceDataSize_Player(audio->player);

     }

 }

- iConstForEach(PtrArray, n, &d->downloads) {

+ iConstForEach(PtrArray, n, &d->items[download_MediaType]) {

     const iGmDownload *down = n.ptr;

     memSize += down->numBytes;

 }

 return memSize; 

}



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

- iGmDownload *dl = NULL;

- iMediaId existing = findLinkDownload_Media(d, linkId);

- iBool isNew = iFalse;

- if (!existing) {

- isNew = iTrue;

- dl = new_GmDownload();

- dl->props.linkId = linkId;

- dl->props.isPermanent = iTrue;

- set_String(&dl->props.url, url);

- pushBack_PtrArray(&d->downloads, dl);

+iBool setUrl_Media(iMedia *d, iGmLinkId linkId, enum iMediaType mediaType, const iString *url) {

+ iMediaId existing = findMediaForLink_Media(d, linkId, mediaType);

+ const iBool isNew = !existing.id;

+ iGmMediaProps *props = NULL;

+ if (mediaType == download_MediaType) {

+ iGmDownload *dl = NULL;

+ if (isNew) {

+ dl = new_GmDownload();

+ pushBack_PtrArray(&d->items[download_MediaType], dl);

+ }

+ else {

+ dl = at_PtrArray(&d->items[download_MediaType], index_MediaId(existing));

+ }

+ props = &dl->props;

 }

- else {

- iGmDownload *dl = at_PtrArray(&d->downloads, existing - 1);

- set_String(&dl->props.url, url);

+ else if (mediaType == fontpack_MediaType) {

+ iGmFontpack *fp = NULL;

+ if (isNew) {

+ fp = new_GmFontpack();

+ pushBack_PtrArray(&d->items[fontpack_MediaType], fp);

+ }

+ else {

+ fp = at_PtrArray(&d->items[fontpack_MediaType], index_MediaId(existing));

+ }

+ props = &fp->props;

+ }

+ if (props) {

+ props->linkId = linkId;

+ props->isPermanent = iTrue;

+ set_String(&props->url, url);

 }

 return isNew;

}

@@ -372,16 +455,17 @@ iBool setData_Media(iMedia *d, iGmLinkId linkId, const iString *mime, const iBlo

 const iBool isPartial  = (flags & partialData_MediaFlag) != 0;

 const iBool allowHide  = (flags & allowHide_MediaFlag) != 0;

 const iBool isDeleting = (!mime || !data);

- iMediaId existing = findLinkImage_Media(d, linkId);

+ iMediaId existing = findMediaForLink_Media(d, linkId, none_MediaType);// findLinkImage_Media(d, linkId);

+ const size_t existingIndex = index_MediaId(existing);

 iBool       isNew      = iFalse;

- if (existing) {

+ if (existing.type == image_MediaType) {

     iGmImage *img;

     if (isDeleting) {

- take_PtrArray(&d->images, existing - 1, (void **) &img);

+ take_PtrArray(&d->items[image_MediaType], existingIndex, (void **) &img);

         delete_GmImage(img);

     }

     else {

- img = at_PtrArray(&d->images, existing - 1);

+ img = at_PtrArray(&d->items[image_MediaType], existingIndex);

         iAssert(equal_String(&img->props.mime, mime)); /* MIME cannot change */

         set_Block(&img->partialData, data);

         if (!isPartial) {

@@ -389,14 +473,14 @@ iBool setData_Media(iMedia *d, iGmLinkId linkId, const iString *mime, const iBlo

         }

     }

 }

- else if ((existing = findLinkAudio_Media(d, linkId)) != 0) {

+ else if (existing.type == audio_MediaType) {

     iGmAudio *audio;

     if (isDeleting) {

- take_PtrArray(&d->audio, existing - 1, (void **) &audio);

+ take_PtrArray(&d->items[audio_MediaType], existingIndex, (void **) &audio);

         delete_GmAudio(audio);

     }

     else {

- audio = at_PtrArray(&d->audio, existing - 1);

+ audio = at_PtrArray(&d->items[audio_MediaType], existingIndex);

         iAssert(equal_String(&audio->props.mime, mime)); /* MIME cannot change */

         updateSourceData_Player(audio->player, mime, data, append_PlayerUpdate);

         if (!isPartial) {

@@ -408,14 +492,14 @@ iBool setData_Media(iMedia *d, iGmLinkId linkId, const iString *mime, const iBlo

         }

     }

 }

- else if ((existing = findLinkDownload_Media(d, linkId)) != 0) {

+ else if (existing.type == download_MediaType) {

     iGmDownload *dl;

     if (isDeleting) {

- take_PtrArray(&d->downloads, existing - 1, (void **) &dl);

+ take_PtrArray(&d->items[download_MediaType], existingIndex, (void **) &dl);

         delete_GmDownload(dl);

     }

     else {

- dl = at_PtrArray(&d->downloads, existing - 1);

+ dl = at_PtrArray(&d->items[download_MediaType], existingIndex);

         if (isEmpty_String(&dl->props.mime)) {

             set_String(&dl->props.mime, mime);

         }

@@ -428,6 +512,21 @@ iBool setData_Media(iMedia *d, iGmLinkId linkId, const iString *mime, const iBlo

         }

     }

 }

+ else if (existing.type == fontpack_MediaType) {

+ iGmFontpack *fp;

+ if (isDeleting) {

+ take_PtrArray(&d->items[fontpack_MediaType], existingIndex, (void **) &fp);

+ delete_GmFontpack(fp);

+ }

+ else {

+ iAssert(!isPartial);

+ fp = at_PtrArray(&d->items[fontpack_MediaType], existingIndex);

+ if (isEmpty_String(&fp->props.mime)) {

+ set_String(&fp->props.mime, mime);

+ }

+ loadData_GmFontpack_(fp, data);

+ }

+ }

 else if (!isDeleting) {

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

         /* Copy the image to a texture. */

@@ -435,7 +534,7 @@ iBool setData_Media(iMedia *d, iGmLinkId linkId, const iString *mime, const iBlo

         img->props.linkId = linkId; /* TODO: use a hash? */

         img->props.isPermanent = !allowHide;

         set_String(&img->props.mime, mime);

- pushBack_PtrArray(&d->images, img);

+ pushBack_PtrArray(&d->items[image_MediaType], img);

         if (!isPartial) {

             makeTexture_GmImage(img);

         }

@@ -450,7 +549,7 @@ iBool setData_Media(iMedia *d, iGmLinkId linkId, const iString *mime, const iBlo

         if (!isPartial) {

             updateSourceData_Player(audio->player, NULL, NULL, complete_PlayerUpdate);

         }

- pushBack_PtrArray(&d->audio, audio);

+ pushBack_PtrArray(&d->items[audio_MediaType], audio);

         /* Start playing right away. */

         start_Player(audio->player);

         postCommandf_App("media.player.started player:%p", audio->player);

@@ -460,125 +559,135 @@ iBool setData_Media(iMedia *d, iGmLinkId linkId, const iString *mime, const iBlo

 return isNew;

}



-iMediaId findLinkImage_Media(const iMedia *d, iGmLinkId linkId) {

- /* TODO: use a hash */

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

- const iGmImage *img = i.ptr;

- if (img->props.linkId == linkId) {

- return index_PtrArrayConstIterator(&i) + 1;

+static iMediaId findMediaPtr_Media_(const iPtrArray *items, enum iMediaType mediaType, iGmLinkId linkId) {

+ iConstForEach(PtrArray, i, items) {

+ const iGmMediaProps *props = i.ptr;

+ if (props->linkId == linkId) {

+ return (iMediaId){

+ .type = mediaType,

+ .id = index_PtrArrayConstIterator(&i) + 1

+ };

     }

 }

- return 0;

-}

-

-size_t numAudio_Media(const iMedia *d) {

- return size_PtrArray(&d->audio);

+ return iInvalidMediaId;

}



-iMediaId findLinkAudio_Media(const iMedia *d, iGmLinkId linkId) {

- /* TODO: use a hash */

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

- const iGmAudio *audio = i.ptr;

- if (audio->props.linkId == linkId) {

- return index_PtrArrayConstIterator(&i) + 1;

+iMediaId findMediaForLink_Media(const iMedia *d, iGmLinkId linkId, enum iMediaType mediaType) {

+ /* TODO: Use hashes, this will get very slow if there is a large number of media items. */

+ iMediaId mid;

+ for (int i = 0; i < max_MediaType; i++) {

+ if (mediaType == i || !mediaType) {

+ mid = findMediaPtr_Media_(&d->items[i], i, linkId);

+ if (mid.type) {

+ return mid;

+ }

     }

 }

- return 0;

+ return iInvalidMediaId;

}



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

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

- const iGmDownload *dl = i.ptr;

- if (dl->props.linkId == linkId) {

- return index_PtrArrayConstIterator(&i) + 1;

- }

- }

- return 0;

+size_t numAudio_Media(const iMedia *d) {

+ return size_PtrArray(&d->items[audio_MediaType]);

}



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

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

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

+ iAssert(imageId.type == image_MediaType);

+ const size_t index = index_MediaId(imageId);

+ if (index < size_PtrArray(&d->items[image_MediaType])) {

+ const iGmImage *img = constAt_PtrArray(&d->items[image_MediaType], index);

     return img->size;

 }

 return zero_I2();

}



-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);

+SDL_Texture *imageTexture_Media(const iMedia *d, iMediaId imageId) {

+ iAssert(imageId.type == image_MediaType);

+ const size_t index = index_MediaId(imageId);

+ if (index < size_PtrArray(&d->items[image_MediaType])) {

+ const iGmImage *img = constAt_PtrArray(&d->items[image_MediaType], index);

     return img->texture;

 }

 return NULL;

}



-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->type = cstr_String(&img->props.mime);

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

- return iTrue;

+iBool info_Media(const iMedia *d, iMediaId mediaId, iGmMediaInfo *info_out) {

+ /* TODO: Use a hash. */

+ const size_t index = index_MediaId(mediaId);

+ switch (mediaId.type) {

+ case image_MediaType:

+ if (index < size_PtrArray(&d->items[image_MediaType])) {

+ const iGmImage *img = constAt_PtrArray(&d->items[image_MediaType], index);

+ info_out->numBytes = img->numBytes;

+ info_out->type = cstr_String(&img->props.mime);

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

+ return iTrue;

+ }

+ break;

+ case audio_MediaType:

+ if (index < size_PtrArray(&d->items[audio_MediaType])) {

+ const iGmAudio *audio = constAt_PtrArray(&d->items[audio_MediaType], index);

+ info_out->type = cstr_String(&audio->props.mime);

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

+ return iTrue;

+ }

+ break;

+ case download_MediaType:

+ if (index < size_PtrArray(&d->items[download_MediaType])) {

+ const iGmDownload *dl = constAt_PtrArray(&d->items[download_MediaType], index);

+ info_out->type = cstr_String(&dl->props.mime);

+ info_out->isPermanent = dl->props.isPermanent;

+ info_out->numBytes = dl->numBytes;

+ return iTrue;

+ }

+ break;

+ case fontpack_MediaType:

+ /* TODO */

+ break;

+ default:

+ break;

 }

 iZap(*info_out);

 return iFalse;

}



iPlayer *audioData_Media(const iMedia *d, iMediaId audioId) {

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

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

+ iAssert(audioId.type == audio_MediaType);

+ const size_t index = index_MediaId(audioId);

+ if (index < size_PtrArray(&d->items[audio_MediaType])) {

+ const iGmAudio *audio = constAt_PtrArray(&d->items[audio_MediaType], index);

     return audio->player;

 }

 return NULL;

}



-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->type = cstr_String(&audio->props.mime);

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

- return iTrue;

- }

- iZap(*info_out);

- return iFalse;

-}

-

iPlayer *audioPlayer_Media(const iMedia *d, iMediaId audioId) {

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

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

+ iAssert(audioId.type == audio_MediaType);

+ const size_t index = index_MediaId(audioId);

+ if (index < size_PtrArray(&d->items[audio_MediaType])) {

+ const iGmAudio *audio = constAt_PtrArray(&d->items[audio_MediaType], index);

     return audio->player;

 }

 return NULL;

}



void pauseAllPlayers_Media(const iMedia *d, iBool setPaused) {

- for (size_t i = 0; i < size_PtrArray(&d->audio); ++i) {

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

+ for (size_t i = 0; i < size_PtrArray(&d->items[audio_MediaType]); ++i) {

+ const iGmAudio *audio = constAt_PtrArray(&d->items[audio_MediaType], i);

     if (audio->player) {

         setPaused_Player(audio->player, setPaused);

     }

 }

}



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

- if (downloadId > 0 && downloadId <= size_PtrArray(&d->downloads)) {

- const iGmDownload *dl = constAt_PtrArray(&d->downloads, downloadId - 1);

- info_out->type = cstr_String(&dl->props.mime);

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

- info_out->numBytes = dl->numBytes;

- return iTrue;

- }

- iZap(*info_out);

- return iFalse;

-}

-

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

                      float *bytesPerSecond_out, iBool *isFinished_out) {

- *path_out = NULL;

+ iAssert(downloadId.type == download_MediaType);

+ *path_out = NULL;

 *bytesPerSecond_out = 0.0f;

- *isFinished_out = iFalse;

- if (downloadId > 0 && downloadId <= size_PtrArray(&d->downloads)) {

- const iGmDownload *dl = constAt_PtrArray(&d->downloads, downloadId - 1);

+ *isFinished_out = iFalse;

+ const size_t index = index_MediaId(downloadId);

+ if (index < size_PtrArray(&d->items[download_MediaType])) {

+ const iGmDownload *dl = constAt_PtrArray(&d->items[download_MediaType], index);

     if (dl->path) {

         *path_out = dl->path;

     }

@@ -587,6 +696,16 @@ void downloadStats_Media(const iMedia *d, iMediaId downloadId, const iString **p

 }

}



+void fontpackInfo_Media(const iMedia *d, iMediaId fontpackId, iFontpackMediaInfo *info_out) {

+ iAssert(fontpackId.type == fontpack_MediaType);

+ iZap(*info_out);

+ const size_t index = index_MediaId(fontpackId);

+ if (index < size_PtrArray(&d->items[fontpack_MediaType])) {

+ const iGmFontpack *fp = constAt_PtrArray(&d->items[fontpack_MediaType], index);

+ *info_out = fp->info;

+ }

+}

+

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



static void updated_MediaRequest_(iAnyObject *obj) {

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

index f7ad6efd..47a4da93 100644

--- a/src/media.h

+++ b/src/media.h

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



#pragma once



+#include "fontpack.h"

+

#include <the_Foundation/block.h>

#include <the_Foundation/string.h>

#include <the_Foundation/vec2.h>

#include <SDL_render.h>



-typedef uint16_t iMediaId;

-

iDeclareType(Player)

iDeclareType(GmMediaInfo)



@@ -38,6 +38,7 @@ struct Impl_GmMediaInfo {

 iBool       isPermanent;

};



+iDeclareType(MediaId)

iDeclareType(Media)

iDeclareTypeConstruction(Media)



@@ -46,28 +47,81 @@ enum iMediaFlags {

 partialData_MediaFlag = iBit(2),

};



-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);

-

-size_t memorySize_Media (const iMedia *);

+enum iMediaType { /* Note: There is a limited number of bits for these; see GmRun below. */

+ none_MediaType,

+ image_MediaType,

+ //animatedImage_MediaType, /* TODO */

+ audio_MediaType,

+ download_MediaType,

+ fontpack_MediaType,

+ max_MediaType

+};



-iMediaId findLinkImage_Media (const iMedia *, uint16_t linkId);

-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);

+struct Impl_MediaId {

+ enum iMediaType type;

+ uint16_t id; /* see GmRun for actually used number of bits */

+};



-size_t numAudio_Media (const iMedia *);

-iMediaId findLinkAudio_Media (const iMedia *, uint16_t linkId);

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

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

-void pauseAllPlayers_Media(const iMedia *, iBool setPaused);

+iLocalDef size_t index_MediaId(const iMediaId mediaId) {

+ return (size_t) mediaId.id - 1;

+}

+

+#define iInvalidMediaId (iMediaId){ none_MediaType, 0 }

+

+void clear_Media (iMedia *);

+iBool setUrl_Media (iMedia *, uint16_t linkId, enum iMediaType mediaType, const iString *url);

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

+

+size_t memorySize_Media (const iMedia *);

+iMediaId findMediaForLink_Media (const iMedia *, uint16_t linkId, enum iMediaType mediaType);

+

+iMediaId id_Media (const iMedia *, uint16_t linkId, enum iMediaType type);

+iBool info_Media (const iMedia *, iMediaId mediaId, iGmMediaInfo *info_out);

+

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

+ return findMediaForLink_Media(d, linkId, image_MediaType);

+}

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

+ return findMediaForLink_Media(d, linkId, audio_MediaType);

+}

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

+ return findMediaForLink_Media(d, linkId, download_MediaType);

+}

+

+iLocalDef iBool imageInfo_Media(const iMedia *d, uint16_t mediaId, iGmMediaInfo *info_out) {

+ return info_Media(d, (iMediaId){ image_MediaType, mediaId }, info_out);

+}

+iLocalDef iBool audioInfo_Media(const iMedia *d, uint16_t mediaId, iGmMediaInfo *info_out) {

+ return info_Media(d, (iMediaId){ audio_MediaType, mediaId }, info_out);

+}

+iLocalDef iBool downloadInfo_Media(const iMedia *d, uint16_t mediaId, iGmMediaInfo *info_out) {

+ return info_Media(d, (iMediaId){ download_MediaType, mediaId }, info_out);

+}

+

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

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

+

+size_t numAudio_Media (const iMedia *);

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

+void pauseAllPlayers_Media (const iMedia *, iBool setPaused);



-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,

                                      float *bytesPerSecond_out, iBool *isFinished_out);



+iDeclareType(FontpackMediaInfo)

+

+struct Impl_FontpackMediaInfo {

+ iFontPackId packId;

+ iBool isValid;

+ iBool isInstalled;

+ iBool isReadOnly;

+ size_t sizeInBytes;

+ iStringList *names;

+};

+

+void fontpackInfo_Media (const iMedia *, iMediaId fontpackId,

+ iFontpackMediaInfo *info_out);

+

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



iDeclareType(GmRequest)

@@ -78,7 +132,7 @@ iDeclareClass(MediaRequest)

struct Impl_MediaRequest {

 iObject          object;

 iDocumentWidget *doc;

- unsigned int linkId;

+ unsigned int linkId; 

 iGmRequest *     req;

};



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

index 45a8cf2d..44db3e5b 100644

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

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

@@ -400,7 +400,18 @@ void init_DocumentWidget(iDocumentWidget *d) {

 addAction_Widget(w, navigateRoot_KeyShortcut, "navigate.root");

}



+void cancelAllRequests_DocumentWidget(iDocumentWidget *d) {

+ iForEach(ObjectList, i, d->media) {

+ iMediaRequest *mr = i.object;

+ cancel_GmRequest(mr->req);

+ }

+ if (d->request) {

+ cancel_GmRequest(d->request);

+ }

+}

+

void deinit_DocumentWidget(iDocumentWidget *d) {

+ cancelAllRequests_DocumentWidget(d);

 pauseAllPlayers_Media(media_GmDocument(d->doc), iTrue);

 removeTicker_App(animate_DocumentWidget_, d);

 removeTicker_App(prerender_DocumentWidget_, d);

@@ -564,7 +575,8 @@ static void addVisible_DocumentWidget_(void *context, const iGmRun *run) {

         pushBack_PtrArray(&d->visibleWideRuns, run);

     }

 }

- if (run->mediaType == audio_GmRunMediaType || run->mediaType == download_GmRunMediaType) {

+ /* Image runs are static so they're drawn as part of the content. */

+ if (run->mediaType && run->mediaType != image_MediaType) {

     iAssert(run->mediaId);

     pushBack_PtrArray(&d->visibleMedia, run);

 }

@@ -758,14 +770,14 @@ static uint32_t mediaUpdateInterval_DocumentWidget_(const iDocumentWidget *d) {

 uint32_t interval = invalidInterval_;

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

     const iGmRun *run = i.ptr;

- if (run->mediaType == audio_GmRunMediaType) {

- iPlayer *plr = audioPlayer_Media(media_GmDocument(d->doc), run->mediaId);

+ if (run->mediaType == audio_MediaType) {

+ iPlayer *plr = audioPlayer_Media(media_GmDocument(d->doc), mediaId_GmRun(run));

         if (flags_Player(plr) & adjustingVolume_PlayerFlag ||

             (isStarted_Player(plr) && !isPaused_Player(plr))) {

             interval = iMin(interval, 1000 / 15);

         }

     }

- else if (run->mediaType == download_GmRunMediaType) {

+ else if (run->mediaType == download_MediaType) {

         interval = iMin(interval, 1000);

     }

 }

@@ -784,8 +796,8 @@ static void updateMedia_DocumentWidget_(iDocumentWidget *d) {

     refresh_Widget(d);

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

         const iGmRun *run = i.ptr;

- if (run->mediaType == audio_GmRunMediaType) {

- iPlayer *plr = audioPlayer_Media(media_GmDocument(d->doc), run->mediaId);

+ if (run->mediaType == audio_MediaType) {

+ iPlayer *plr = audioPlayer_Media(media_GmDocument(d->doc), mediaId_GmRun(run));

             if (idleTimeMs_Player(plr) > 3000 && ~flags_Player(plr) & volumeGrabbed_PlayerFlag &&

                 flags_Player(plr) & adjustingVolume_PlayerFlag) {

                 setFlags_Player(plr, adjustingVolume_PlayerFlag, iFalse);

@@ -1244,6 +1256,9 @@ static const char *zipPageHeading_(const iRangecc mime) {

 if (equalCase_Rangecc(mime, "application/gpub+zip")) {

     return book_Icon " Gempub";

 }

+ else if (equalCase_Rangecc(mime, mimeType_FontPack)) {

+ return "\U0001f520 Fontpack";

+ }

 iRangecc type = iNullRange;

 nextSplit_Rangecc(mime, "/", &type); /* skip the part before the slash */

 nextSplit_Rangecc(mime, "/", &type);

@@ -1258,165 +1273,175 @@ static const char *zipPageHeading_(const iRangecc mime) {



static void postProcessRequestContent_DocumentWidget_(iDocumentWidget *d, iBool isCached) {

 iWidget *w = as_Widget(d);

- delete_Gempub(d->sourceGempub);

- d->sourceGempub = NULL;

- if (!cmpCase_String(&d->sourceMime, "application/octet-stream") ||

- !cmpCase_String(&d->sourceMime, mimeType_Gempub) ||

- endsWithCase_String(d->mod.url, ".gpub")) {

- iGempub *gempub = new_Gempub();

- if (open_Gempub(gempub, &d->sourceContent)) {

- setBaseUrl_Gempub(gempub, d->mod.url);

- setSource_DocumentWidget(d, collect_String(coverPageSource_Gempub(gempub)));

- setCStr_String(&d->sourceMime, mimeType_Gempub);

- d->sourceGempub = gempub;

- }

- else {

- delete_Gempub(gempub);

- }

- }

- if (!d->sourceGempub) {

- const iString *localPath = collect_String(localFilePathFromUrl_String(d->mod.url));

- iBool isInside = iFalse;

- if (localPath && !fileExists_FileInfo(localPath)) {

- /* This URL may refer to a file inside the archive. */

- localPath = findContainerArchive_Path(localPath);

- isInside = iTrue;

- }

- if (localPath && equal_CStr(mediaType_Path(localPath), "application/gpub+zip")) {

+ /* Gempub page behavior and footer actions. */ {

+ /* TODO: move this to gempub.c */

+ delete_Gempub(d->sourceGempub);

+ d->sourceGempub = NULL;

+ if (!cmpCase_String(&d->sourceMime, "application/octet-stream") ||

+ !cmpCase_String(&d->sourceMime, mimeType_Gempub) ||

+ endsWithCase_String(d->mod.url, ".gpub")) {

         iGempub *gempub = new_Gempub();

- if (openFile_Gempub(gempub, localPath)) {

- setBaseUrl_Gempub(gempub, collect_String(makeFileUrl_String(localPath)));

- if (!isInside) {

- setSource_DocumentWidget(d, collect_String(coverPageSource_Gempub(gempub)));

- setCStr_String(&d->sourceMime, mimeType_Gempub);

- }

+ if (open_Gempub(gempub, &d->sourceContent)) {

+ setBaseUrl_Gempub(gempub, d->mod.url);

+ setSource_DocumentWidget(d, collect_String(coverPageSource_Gempub(gempub)));

+ setCStr_String(&d->sourceMime, mimeType_Gempub);

             d->sourceGempub = gempub;

         }

         else {

             delete_Gempub(gempub);

         }

     }

- }

- if (d->sourceGempub) {

- if (equal_String(d->mod.url, coverPageUrl_Gempub(d->sourceGempub))) {

- if (!isRemote_Gempub(d->sourceGempub)) {

- iArray *items = collectNew_Array(sizeof(iMenuItem));

- pushBack_Array(

- items,

- &(iMenuItem){ book_Icon " ${gempub.cover.view}",

- 0,

- 0,

- format_CStr("!open url:%s",

- cstr_String(indexPageUrl_Gempub(d->sourceGempub))) });

- if (navSize_Gempub(d->sourceGempub) > 0) {

- pushBack_Array(

- items,

- &(iMenuItem){

- format_CStr(forwardArrow_Icon " %s",

- cstr_String(navLinkLabel_Gempub(d->sourceGempub, 0))),

- SDLK_RIGHT,

- 0,

- format_CStr("!open url:%s",

- cstr_String(navLinkUrl_Gempub(d->sourceGempub, 0))) });

- }

- makeFooterButtons_DocumentWidget_(d, constData_Array(items), size_Array(items));

+ if (!d->sourceGempub) {

+ const iString *localPath = collect_String(localFilePathFromUrl_String(d->mod.url));

+ iBool isInside = iFalse;

+ if (localPath && !fileExists_FileInfo(localPath)) {

+ /* This URL may refer to a file inside the archive. */

+ localPath = findContainerArchive_Path(localPath);

+ isInside = iTrue;

         }

- else {

- makeFooterButtons_DocumentWidget_(

- d,

- (iMenuItem[]){ { book_Icon " ${menu.save.downloads.open}",

- SDLK_s,

- KMOD_PRIMARY | KMOD_SHIFT,

- "document.save open:1" },

- { download_Icon " " saveToDownloads_Label,

- SDLK_s,

- KMOD_PRIMARY,

- "document.save" } },

- 2);

- }

- if (preloadCoverImage_Gempub(d->sourceGempub, d->doc)) {

- redoLayout_GmDocument(d->doc);

- updateVisible_DocumentWidget_(d);

- invalidate_DocumentWidget_(d);

+ if (localPath && equal_CStr(mediaType_Path(localPath), mimeType_Gempub)) {

+ iGempub *gempub = new_Gempub();

+ if (openFile_Gempub(gempub, localPath)) {

+ setBaseUrl_Gempub(gempub, collect_String(makeFileUrl_String(localPath)));

+ if (!isInside) {

+ setSource_DocumentWidget(d, collect_String(coverPageSource_Gempub(gempub)));

+ setCStr_String(&d->sourceMime, mimeType_Gempub);

+ }

+ d->sourceGempub = gempub;

+ }

+ else {

+ delete_Gempub(gempub);

+ }

         }

     }

- else if (equal_String(d->mod.url, indexPageUrl_Gempub(d->sourceGempub))) {

- makeFooterButtons_DocumentWidget_(

- d,

- (iMenuItem[]){ { format_CStr(book_Icon " %s",

- cstr_String(property_Gempub(d->sourceGempub,

- title_GempubProperty))),

- SDLK_LEFT,

- 0,

- format_CStr("!open url:%s",

- cstr_String(coverPageUrl_Gempub(d->sourceGempub))) } },

- 1);

- }

- else {

- /* Navigation buttons. */

- iArray *items = collectNew_Array(sizeof(iMenuItem));

- const size_t navIndex = navIndex_Gempub(d->sourceGempub, d->mod.url);

- if (navIndex != iInvalidPos) {

- if (navIndex < navSize_Gempub(d->sourceGempub) - 1) {

+ if (d->sourceGempub) {

+ if (equal_String(d->mod.url, coverPageUrl_Gempub(d->sourceGempub))) {

+ if (!isRemote_Gempub(d->sourceGempub)) {

+ iArray *items = collectNew_Array(sizeof(iMenuItem));

                 pushBack_Array(

                     items,

- &(iMenuItem){

- format_CStr(forwardArrow_Icon " %s",

- cstr_String(navLinkLabel_Gempub(d->sourceGempub, navIndex + 1))),

- SDLK_RIGHT,

- 0,

- format_CStr("!open url:%s",

- cstr_String(navLinkUrl_Gempub(d->sourceGempub, navIndex + 1))) });

+ &(iMenuItem){ book_Icon " ${gempub.cover.view}",

+ 0,

+ 0,

+ format_CStr("!open url:%s",

+ cstr_String(indexPageUrl_Gempub(d->sourceGempub))) });

+ if (navSize_Gempub(d->sourceGempub) > 0) {

+ pushBack_Array(

+ items,

+ &(iMenuItem){

+ format_CStr(forwardArrow_Icon " %s",

+ cstr_String(navLinkLabel_Gempub(d->sourceGempub, 0))),

+ SDLK_RIGHT,

+ 0,

+ format_CStr("!open url:%s",

+ cstr_String(navLinkUrl_Gempub(d->sourceGempub, 0))) });

+ }

+ makeFooterButtons_DocumentWidget_(d, constData_Array(items), size_Array(items));

             }

- if (navIndex > 0) {

- pushBack_Array(

- items,

- &(iMenuItem){

- format_CStr(backArrow_Icon " %s",

- cstr_String(navLinkLabel_Gempub(d->sourceGempub, navIndex - 1))),

- SDLK_LEFT,

- 0,

- format_CStr("!open url:%s",

- cstr_String(navLinkUrl_Gempub(d->sourceGempub, navIndex - 1))) });

+ else {

+ makeFooterButtons_DocumentWidget_(

+ d,

+ (iMenuItem[]){ { book_Icon " ${menu.save.downloads.open}",

+ SDLK_s,

+ KMOD_PRIMARY | KMOD_SHIFT,

+ "document.save open:1" },

+ { download_Icon " " saveToDownloads_Label,

+ SDLK_s,

+ KMOD_PRIMARY,

+ "document.save" } },

+ 2);

             }

- else if (!equalCase_String(d->mod.url, indexPageUrl_Gempub(d->sourceGempub))) {

- pushBack_Array(

- items,

- &(iMenuItem){

- format_CStr(book_Icon " %s",

- cstr_String(property_Gempub(d->sourceGempub, title_GempubProperty))),

- SDLK_LEFT,

- 0,

- format_CStr("!open url:%s",

- cstr_String(coverPageUrl_Gempub(d->sourceGempub))) });

+ if (preloadCoverImage_Gempub(d->sourceGempub, d->doc)) {

+ redoLayout_GmDocument(d->doc);

+ updateVisible_DocumentWidget_(d);

+ invalidate_DocumentWidget_(d);

             }

         }

- if (!isEmpty_Array(items)) {

- makeFooterButtons_DocumentWidget_(d, constData_Array(items), size_Array(items)); 

+ else if (equal_String(d->mod.url, indexPageUrl_Gempub(d->sourceGempub))) {

+ makeFooterButtons_DocumentWidget_(

+ d,

+ (iMenuItem[]){ { format_CStr(book_Icon " %s",

+ cstr_String(property_Gempub(d->sourceGempub,

+ title_GempubProperty))),

+ SDLK_LEFT,

+ 0,

+ format_CStr("!open url:%s",

+ cstr_String(coverPageUrl_Gempub(d->sourceGempub))) } },

+ 1);

         }

- }

- if (!isCached && prefs_App()->pinSplit &&

- equal_String(d->mod.url, indexPageUrl_Gempub(d->sourceGempub))) {

- const iString *navStart = navStartLinkUrl_Gempub(d->sourceGempub);

- if (navStart) {

- iWindow *win = get_Window();

- /* Auto-split to show index and the first navigation link. */

- if (numRoots_Window(win) == 2) {

- /* This document is showing the index page. */

- iRoot *other = otherRoot_Window(win, w->root);

- postCommandf_Root(other, "open url:%s", cstr_String(navStart));

- if (prefs_App()->pinSplit == 1 && w->root == win->roots[1]) {

- /* On the wrong side. */

Proxy Information
Original URL
gemini://git.skyjake.fi/lagrange/work%2Fv1.8/pcdiff/960df03c17091aca37f53eaab8fc27c669d26a5e
Status Code
Success (20)
Meta
text/plain
Capsule Response Time
88.025376 milliseconds
Gemini-to-HTML Time
28.808146 milliseconds

This content has been proxied by September (ba2dc).