summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--res/arabic.fontpack/fontpack.ini2
-rw-r--r--res/cjk.fontpack/fontpack.ini2
-rw-r--r--res/default.fontpack/fontpack.ini2
-rw-r--r--res/firasans.fontpack/fontpack.ini2
-rw-r--r--res/literata.fontpack/fontpack.ini2
-rw-r--r--res/nunito.fontpack/fontpack.ini2
-rw-r--r--res/tinos.fontpack/fontpack.ini2
-rw-r--r--src/app.c1
-rw-r--r--src/defs.h4
-rw-r--r--src/fontpack.c299
-rw-r--r--src/fontpack.h65
-rw-r--r--src/gempub.c2
-rw-r--r--src/gmdocument.c159
-rw-r--r--src/gmdocument.h16
-rw-r--r--src/gmrequest.c9
-rw-r--r--src/gmutil.c8
-rw-r--r--src/media.c349
-rw-r--r--src/media.h92
-rw-r--r--src/ui/documentwidget.c429
-rw-r--r--src/ui/documentwidget.h2
-rw-r--r--src/ui/mediaui.c88
-rw-r--r--src/ui/mediaui.h20
22 files changed, 1030 insertions, 527 deletions
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 @@
1version = 1
2
1[arabic] 3[arabic]
2name = "Noto Sans Arabic UI" 4name = "Noto Sans Arabic UI"
3auxiliary = true 5auxiliary = 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 @@
1version = 1
2
1[notosansjp] 3[notosansjp]
2name = "Noto Sans JP" 4name = "Noto Sans JP"
3auxiliary = true 5auxiliary = 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 @@
17# `glyphscale` and `voffset` can also be specified separately for the UI and 17# `glyphscale` and `voffset` can also be specified separately for the UI and
18# document domains by prefixing `ui.` or `doc.` to the key. 18# document domains by prefixing `ui.` or `doc.` to the key.
19 19
20version = 1
21
20[default] 22[default]
21name = "Source Sans" 23name = "Source Sans"
22regular = "SourceSans3-Regular.ttf" 24regular = "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 @@
1version = 1
2
1[firasans] 3[firasans]
2name = "Fira Sans" 4name = "Fira Sans"
3glyphscale = 0.85 5glyphscale = 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 @@
1version = 1
2
1[literata] 3[literata]
2name = "Literata" 4name = "Literata"
3regular = "Literata-Regular-opsz=14.ttf" 5regular = "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 @@
1version = 1
2
1[nunito] 3[nunito]
2name = "Nunito" 4name = "Nunito"
3tweaks = 0x1 # some hardcoded kerning changes (`Th`, etc.) 5tweaks = 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 @@
1version = 1
2
1[tinos] 3[tinos]
2name = "Tinos" 4name = "Tinos"
3glyphscale = 0.850 5glyphscale = 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) {
2669 const iBool isSplit = numRoots_Window(get_Window()) > 1; 2669 const iBool isSplit = numRoots_Window(get_Window()) > 1;
2670 if (tabCount_Widget(tabs) > 1 || isSplit) { 2670 if (tabCount_Widget(tabs) > 1 || isSplit) {
2671 iWidget *closed = removeTabPage_Widget(tabs, index); 2671 iWidget *closed = removeTabPage_Widget(tabs, index);
2672 cancelAllRequests_DocumentWidget((iDocumentWidget *) closed);
2672 destroy_Widget(closed); /* released later */ 2673 destroy_Widget(closed); /* released later */
2673 if (index == tabCount_Widget(tabs)) { 2674 if (index == tabCount_Widget(tabs)) {
2674 index--; 2675 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) {
125#define delete_Icon "\u232b" 125#define delete_Icon "\u232b"
126#define copy_Icon "\u2398" //"\u2bba" 126#define copy_Icon "\u2398" //"\u2bba"
127#define check_Icon "\u2714" 127#define check_Icon "\u2714"
128#define ballotCheck_Icon "\U0001f5f9" 128#define ballotChecked_Icon "\U0001f5f9"
129#define ballotUnchecked_Icon "\U0001f5f9"
129#define inbox_Icon "\U0001f4e5" 130#define inbox_Icon "\U0001f4e5"
130#define book_Icon "\U0001f56e" 131#define book_Icon "\U0001f56e"
131#define bookmark_Icon "\U0001f516" 132#define bookmark_Icon "\U0001f516"
132#define folder_Icon "\U0001f4c1" 133#define folder_Icon "\U0001f4c1"
134#define file_Icon "\U0001f5ce"
133#define openTab_Icon "\u2750" 135#define openTab_Icon "\u2750"
134#define openTabBg_Icon "\u2b1a" 136#define openTabBg_Icon "\u2b1a"
135#define openExt_Icon "\u27a0" 137#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. */
33#include <the_Foundation/string.h> 33#include <the_Foundation/string.h>
34#include <the_Foundation/toml.h> 34#include <the_Foundation/toml.h>
35 35
36/* TODO: Clean up and/or reorder this file, it's a bit unorganized. */ 36const char *mimeType_FontPack = "application/lagrange-fontpack+zip";
37 37
38float scale_FontSize(enum iFontSize size) { 38float scale_FontSize(enum iFontSize size) {
39 static const float sizes[max_FontSize] = { 39 static const float sizes[max_FontSize] = {
@@ -57,38 +57,9 @@ float scale_FontSize(enum iFontSize size) {
57 return sizes[size]; 57 return sizes[size];
58} 58}
59 59
60iDeclareType(Fonts)
61
62struct Impl_Fonts {
63 iString userDir;
64 iPtrArray packs;
65 iPtrArray files;
66 iPtrArray specOrder; /* specs sorted by priority */
67};
68
69static iFonts fonts_;
70
71static void unloadFiles_Fonts_(iFonts *d) {
72 /* TODO: Mark all files in font packs as not resident. */
73 iForEach(PtrArray, i, &d->files) {
74 delete_FontFile(i.ptr);
75 }
76 clear_PtrArray(&d->files);
77}
78
79static iFontFile *findFile_Fonts_(iFonts *d, const iString *id) {
80 iForEach(PtrArray, i, &d->files) {
81 iFontFile *ff = i.ptr;
82 if (equal_String(&ff->id, id)) {
83 return ff;
84 }
85 }
86 return NULL;
87}
88
89/*----------------------------------------------------------------------------------------------*/ 60/*----------------------------------------------------------------------------------------------*/
90 61
91iDefineTypeConstruction(FontFile) 62iDefineObjectConstruction(FontFile)
92 63
93void init_FontFile(iFontFile *d) { 64void init_FontFile(iFontFile *d) {
94 init_String(&d->id); 65 init_String(&d->id);
@@ -114,7 +85,7 @@ static void load_FontFile_(iFontFile *d, const iBlock *data) {
114 HB_MEMORY_MODE_READONLY, NULL, NULL); 85 HB_MEMORY_MODE_READONLY, NULL, NULL);
115 d->hbFace = hb_face_create(d->hbBlob, 0); 86 d->hbFace = hb_face_create(d->hbBlob, 0);
116 d->hbFont = hb_font_create(d->hbFace); 87 d->hbFont = hb_font_create(d->hbFace);
117#endif 88#endif
118} 89}
119 90
120static void unload_FontFile_(iFontFile *d) { 91static void unload_FontFile_(iFontFile *d) {
@@ -126,12 +97,13 @@ static void unload_FontFile_(iFontFile *d) {
126 d->hbFont = NULL; 97 d->hbFont = NULL;
127 d->hbFace = NULL; 98 d->hbFace = NULL;
128 d->hbBlob = NULL; 99 d->hbBlob = NULL;
129#endif 100#endif
130 clear_Block(&d->sourceData); 101 clear_Block(&d->sourceData);
131 iZap(d->stbInfo); 102 iZap(d->stbInfo);
132} 103}
133 104
134void deinit_FontFile(iFontFile *d) { 105void deinit_FontFile(iFontFile *d) {
106 printf("FontFile %p {%s} is DESTROYED\n", d, cstr_String(&d->id));
135 unload_FontFile_(d); 107 unload_FontFile_(d);
136 deinit_Block(&d->sourceData); 108 deinit_Block(&d->sourceData);
137 deinit_String(&d->id); 109 deinit_String(&d->id);
@@ -156,12 +128,12 @@ void measureGlyph_FontFile(const iFontFile *d, uint32_t glyphIndex,
156 128
157/*----------------------------------------------------------------------------------------------*/ 129/*----------------------------------------------------------------------------------------------*/
158 130
159
160iDefineTypeConstruction(FontSpec) 131iDefineTypeConstruction(FontSpec)
161 132
162void init_FontSpec(iFontSpec *d) { 133void init_FontSpec(iFontSpec *d) {
163 init_String(&d->id); 134 init_String(&d->id);
164 init_String(&d->name); 135 init_String(&d->name);
136 init_String(&d->sourcePath);
165 d->flags = 0; 137 d->flags = 0;
166 d->priority = 0; 138 d->priority = 0;
167 for (int i = 0; i < 2; ++i) { 139 for (int i = 0; i < 2; ++i) {
@@ -173,52 +145,119 @@ void init_FontSpec(iFontSpec *d) {
173} 145}
174 146
175void deinit_FontSpec(iFontSpec *d) { 147void deinit_FontSpec(iFontSpec *d) {
148 /* FontFile references are held by FontSpecs. */
149 iForIndices(i, d->styles) {
150 iRelease(d->styles[i]);
151 }
152 deinit_String(&d->sourcePath);
176 deinit_String(&d->name); 153 deinit_String(&d->name);
177 deinit_String(&d->id); 154 deinit_String(&d->id);
178} 155}
179 156
180/*----------------------------------------------------------------------------------------------*/ 157/*----------------------------------------------------------------------------------------------*/
181 158
182iDeclareType(FontPack) 159iDeclareType(Fonts)
183iDeclareTypeConstruction(FontPack) 160
161struct Impl_Fonts {
162 iString userDir;
163 iPtrArray packs;
164 iObjectList *files;
165 iPtrArray specOrder; /* specs sorted by priority */
166};
167
168static iFonts fonts_;
169
170static void unloadFiles_Fonts_(iFonts *d) {
171 /* TODO: Mark all files in font packs as not resident. */
172 clear_ObjectList(d->files);
173}
174
175static iFontFile *findFile_Fonts_(iFonts *d, const iString *id) {
176 iForEach(ObjectList, i, d->files) {
177 iFontFile *ff = i.object;
178 if (equal_String(&ff->id, id)) {
179 return ff;
180 }
181 }
182 return NULL;
183}
184
185static void releaseUnusedFiles_Fonts_(iFonts *d) {
186 iForEach(ObjectList, i, d->files) {
187 iFontFile *ff = i.object;
188 if (ff->object.refCount == 1) {
189 /* No specs use this. */
190 //printf("[Fonts] releasing unused font file: %p {%s}\n", ff, cstr_String(&ff->id));
191 remove_ObjectListIterator(&i);
192 }
193 }
194}
195
196/*----------------------------------------------------------------------------------------------*/
184 197
185struct Impl_FontPack { 198struct Impl_FontPack {
186 const iArchive *archive; /* opened ZIP archive */ 199 iString id; /* lowercase filename without the .fontpack extension */
200 int version;
201 iBool isStandalone;
202 iBool isReadOnly;
187 iArray fonts; /* array of FontSpecs */ 203 iArray fonts; /* array of FontSpecs */
204 const iArchive *archive; /* opened ZIP archive */
188 iString * loadPath; 205 iString * loadPath;
189 iFontSpec * loadSpec; 206 iFontSpec * loadSpec;
190}; 207};
191 208
209iDefineTypeConstruction(FontPack)
210
192void init_FontPack(iFontPack *d) { 211void init_FontPack(iFontPack *d) {
193 d->archive = NULL; 212 init_String(&d->id);
213 d->version = 0;
214 d->isStandalone = iFalse;
215 d->isReadOnly = iFalse;
194 init_Array(&d->fonts, sizeof(iFontSpec)); 216 init_Array(&d->fonts, sizeof(iFontSpec));
217 d->archive = NULL;
195 d->loadSpec = NULL; 218 d->loadSpec = NULL;
196 d->loadPath = NULL; 219 d->loadPath = NULL;
197} 220}
198 221
199void deinit_FontPack(iFontPack *d) { 222void deinit_FontPack(iFontPack *d) {
223 iAssert(d->archive == NULL);
224 iAssert(d->loadSpec == NULL);
200 delete_String(d->loadPath); 225 delete_String(d->loadPath);
201 iForEach(Array, i, &d->fonts) { 226 iForEach(Array, i, &d->fonts) {
202 deinit_FontSpec(i.value); 227 deinit_FontSpec(i.value);
203 } 228 }
204 deinit_Array(&d->fonts); 229 deinit_Array(&d->fonts);
205 iAssert(d->archive == NULL); 230 deinit_String(&d->id);
206 iAssert(d->loadSpec == NULL); 231 releaseUnusedFiles_Fonts_(&fonts_);
207} 232}
208 233
209iDefineTypeConstruction(FontPack) 234iFontPackId id_FontPack(const iFontPack *d) {
235 return (iFontPackId){ &d->id, d->version };
236}
237
238const iPtrArray *listSpecs_FontPack(const iFontPack *d) {
239 if (!d) return NULL;
240 iPtrArray *list = collectNew_PtrArray();
241 iConstForEach(Array, i, &d->fonts) {
242 pushBack_PtrArray(list, i.value);
243 }
244 return list;
245}
210 246
211void handleIniTable_FontPack_(void *context, const iString *table, iBool isStart) { 247void handleIniTable_FontPack_(void *context, const iString *table, iBool isStart) {
212 iFontPack *d = context; 248 iFontPack *d = context;
213 if (isStart) { 249 if (isStart) {
214 iAssert(!d->loadSpec); 250 iAssert(!d->loadSpec);
215 /* Each font ID must be unique. */ 251 /* Each font ID must be unique in the non-standalone packs. */
216 if (!findSpec_Fonts(cstr_String(table))) { 252 if (d->isStandalone || !findSpec_Fonts(cstr_String(table))) {
217 d->loadSpec = new_FontSpec(); 253 d->loadSpec = new_FontSpec();
218 set_String(&d->loadSpec->id, table); 254 set_String(&d->loadSpec->id, table);
255 if (d->loadPath) {
256 set_String(&d->loadSpec->sourcePath, d->loadPath);
257 }
219 } 258 }
220 } 259 }
221 else { 260 else if (d->loadSpec) {
222 /* Set fallback font files. */ { 261 /* Set fallback font files. */ {
223 const iFontFile **styles = d->loadSpec->styles; 262 const iFontFile **styles = d->loadSpec->styles;
224 if (!styles[regular_FontStyle]) { 263 if (!styles[regular_FontStyle]) {
@@ -229,11 +268,11 @@ void handleIniTable_FontPack_(void *context, const iString *table, iBool isStart
229 return; 268 return;
230 } 269 }
231 if (!styles[semiBold_FontStyle]) { 270 if (!styles[semiBold_FontStyle]) {
232 styles[semiBold_FontStyle] = styles[bold_FontStyle]; 271 styles[semiBold_FontStyle] = ref_Object(styles[bold_FontStyle]);
233 } 272 }
234 for (size_t s = 0; s < max_FontStyle; s++) { 273 for (size_t s = 0; s < max_FontStyle; s++) {
235 if (!styles[s]) { 274 if (!styles[s]) {
236 styles[s] = styles[regular_FontStyle]; 275 styles[s] = ref_Object(styles[regular_FontStyle]);
237 } 276 }
238 } 277 }
239 } 278 }
@@ -263,7 +302,15 @@ static iBlock *readFile_FontPack_(const iFontPack *d, const iString *path) {
263void handleIniKeyValue_FontPack_(void *context, const iString *table, const iString *key, 302void handleIniKeyValue_FontPack_(void *context, const iString *table, const iString *key,
264 const iTomlValue *value) { 303 const iTomlValue *value) {
265 iFontPack *d = context; 304 iFontPack *d = context;
266 if (!d->loadSpec) return; 305 if (isEmpty_String(table)) {
306 if (!cmp_String(key, "version")) {
307 d->version = number_TomlValue(value);
308 }
309 return;
310 }
311 if (!d->loadSpec) {
312 return;
313 }
267 iUnused(table); 314 iUnused(table);
268 if (!cmp_String(key, "name") && value->type == string_TomlType) { 315 if (!cmp_String(key, "name") && value->type == string_TomlType) {
269 set_String(&d->loadSpec->name, value->value.string); 316 set_String(&d->loadSpec->name, value->value.string);
@@ -322,12 +369,13 @@ void handleIniKeyValue_FontPack_(void *context, const iString *table, const iStr
322 ff = new_FontFile(); 369 ff = new_FontFile();
323 set_String(&ff->id, fontFileId); 370 set_String(&ff->id, fontFileId);
324 load_FontFile_(ff, data); 371 load_FontFile_(ff, data);
325 pushBack_PtrArray(&fonts_.files, ff); /* centralized ownership */ 372 pushBack_ObjectList(fonts_.files, ff); /* centralized ownership */
373 iRelease(ff);
326 delete_Block(data); 374 delete_Block(data);
327// printf("[FontPack] loaded file: %s\n", cstr_String(fontFileId)); 375// printf("[FontPack] loaded file: %s\n", cstr_String(fontFileId));
328 } 376 }
329 } 377 }
330 d->loadSpec->styles[i] = ff; 378 d->loadSpec->styles[i] = ref_Object(ff);
331 delete_String(fontFileId); 379 delete_String(fontFileId);
332 break; 380 break;
333 } 381 }
@@ -348,29 +396,6 @@ static iBool load_FontPack_(iFontPack *d, const iString *ini) {
348 return ok; 396 return ok;
349} 397}
350 398
351#if 0
352iBool loadIniFile_FontPack(iFontPack *d, const iString *iniPath) {
353 iBeginCollect();
354 iBool ok = iFalse;
355 iFile *f = iClob(new_File(iniPath));
356 if (open_File(f, text_FileMode | readOnly_FileMode)) {
357 d->loadPath = collect_String(newRange_String(dirName_Path(iniPath)));
358 iString *src = collect_String(readString_File(f));
359
360 iTomlParser *ini = collect_TomlParser(new_TomlParser());
361 setHandlers_TomlParser(ini, handleIniTable_FontPack_, handleIniKeyValue_FontPack_, d);
362 if (!parse_TomlParser(ini, src)) {
363 fprintf(stderr, "[FontPack] error parsing %s\n", cstr_String(iniPath));
364 }
365 iAssert(d->loadSpec == NULL);
366 d->loadPath = NULL;
367 ok = iTrue;
368 }
369 iEndCollect();
370 return ok;
371}
372#endif
373
374iBool loadArchive_FontPack(iFontPack *d, const iArchive *zip) { 399iBool loadArchive_FontPack(iFontPack *d, const iArchive *zip) {
375 d->archive = zip; 400 d->archive = zip;
376 iBool ok = iFalse; 401 iBool ok = iFalse;
@@ -387,6 +412,28 @@ iBool loadArchive_FontPack(iFontPack *d, const iArchive *zip) {
387 return ok; 412 return ok;
388} 413}
389 414
415void setLoadPath_FontPack(iFontPack *d, const iString *path) {
416 if (!d->loadPath) {
417 d->loadPath = new_String();
418 }
419 set_String(d->loadPath, path);
420 /* Pack ID is based on the file name. */
421 setRange_String(&d->id, baseName_Path(path));
422 setRange_String(&d->id, withoutExtension_Path(&d->id));
423}
424
425void setStandalone_FontPack(iFontPack *d, iBool standalone) {
426 d->isStandalone = standalone;
427}
428
429void setReadOnly_FontPack(iFontPack *d, iBool readOnly) {
430 d->isReadOnly = readOnly;
431}
432
433iBool isReadOnly_FontPack(const iFontPack *d) {
434 return d->isReadOnly;
435}
436
390/*----------------------------------------------------------------------------------------------*/ 437/*----------------------------------------------------------------------------------------------*/
391 438
392static void unloadFonts_Fonts_(iFonts *d) { 439static void unloadFonts_Fonts_(iFonts *d) {
@@ -397,14 +444,23 @@ static void unloadFonts_Fonts_(iFonts *d) {
397 clear_PtrArray(&d->packs); 444 clear_PtrArray(&d->packs);
398} 445}
399 446
447static int cmpName_FontSpecPtr_(const void *a, const void *b) {
448 const iFontSpec **p1 = (const iFontSpec **) a, **p2 = (const iFontSpec **) b;
449 return cmpStringCase_String(&(*p1)->name, &(*p2)->name);
450}
451
400static int cmpPriority_FontSpecPtr_(const void *a, const void *b) { 452static int cmpPriority_FontSpecPtr_(const void *a, const void *b) {
401 const iFontSpec **p1 = (const iFontSpec **) a, **p2 = (const iFontSpec **) b; 453 const iFontSpec **p1 = (const iFontSpec **) a, **p2 = (const iFontSpec **) b;
402 return -iCmp((*p1)->priority, (*p2)->priority); /* highest priority first */ 454 const int cmp = -iCmp((*p1)->priority, (*p2)->priority); /* highest priority first */
455 if (cmp) return cmp;
456 return cmpName_FontSpecPtr_(a, b);
403} 457}
404 458
405static int cmpName_FontSpecPtr_(const void *a, const void *b) { 459static int cmpSourceAndPriority_FontSpecPtr_(const void *a, const void *b) {
406 const iFontSpec **p1 = (const iFontSpec **) a, **p2 = (const iFontSpec **) b; 460 const iFontSpec **p1 = (const iFontSpec **) a, **p2 = (const iFontSpec **) b;
407 return cmpStringCase_String(&(*p1)->name, &(*p2)->name); 461 const int cmp = cmpStringCase_String(&(*p1)->sourcePath, &(*p2)->sourcePath);
462 if (cmp) return cmp;
463 return cmpPriority_FontSpecPtr_(a, b);
408} 464}
409 465
410static void sortSpecs_Fonts_(iFonts *d) { 466static void sortSpecs_Fonts_(iFonts *d) {
@@ -422,11 +478,13 @@ void init_Fonts(const char *userDir) {
422 iFonts *d = &fonts_; 478 iFonts *d = &fonts_;
423 initCStr_String(&d->userDir, userDir); 479 initCStr_String(&d->userDir, userDir);
424 init_PtrArray(&d->packs); 480 init_PtrArray(&d->packs);
425 init_PtrArray(&d->files); 481 d->files = new_ObjectList();
426 init_PtrArray(&d->specOrder); 482 init_PtrArray(&d->specOrder);
427 /* Load the required fonts. */ { 483 /* Load the required fonts. */ {
428 iFontPack *pack = new_FontPack(); 484 iFontPack *pack = new_FontPack();
485 setCStr_String(&pack->id, "default");
429 iArchive *arch = new_Archive(); 486 iArchive *arch = new_Archive();
487 setReadOnly_FontPack(pack, iTrue);
430 openData_Archive(arch, &fontpackDefault_Embedded); 488 openData_Archive(arch, &fontpackDefault_Embedded);
431 loadArchive_FontPack(pack, arch); /* should never fail if we've made it this far */ 489 loadArchive_FontPack(pack, arch); /* should never fail if we've made it this far */
432 iRelease(arch); 490 iRelease(arch);
@@ -436,7 +494,7 @@ void init_Fonts(const char *userDir) {
436 const char *locations[] = { 494 const char *locations[] = {
437 ".", 495 ".",
438 "./fonts", 496 "./fonts",
439 "../share/lagrange", 497 "../share/lagrange", /* Note: These must match CMakeLists.txt install destination */
440 "../../share/lagrange", 498 "../../share/lagrange",
441 concatPath_CStr(userDir, "fonts"), 499 concatPath_CStr(userDir, "fonts"),
442 userDir, 500 userDir,
@@ -450,7 +508,13 @@ void init_Fonts(const char *userDir) {
450 iArchive *arch = new_Archive(); 508 iArchive *arch = new_Archive();
451 if (openFile_Archive(arch, entryPath)) { 509 if (openFile_Archive(arch, entryPath)) {
452 iFontPack *pack = new_FontPack(); 510 iFontPack *pack = new_FontPack();
453 pack->loadPath = copy_String(entryPath); 511 setLoadPath_FontPack(pack, entryPath);
512 setReadOnly_FontPack(pack, !isWritable_FileInfo(entry.value));
513#if defined (iPlatformApple)
514 if (startsWith_String(pack->loadPath, cstr_String(execDir))) {
515 setReadOnly_FontPack(pack, iTrue);
516 }
517#endif
454 if (loadArchive_FontPack(pack, arch)) { 518 if (loadArchive_FontPack(pack, arch)) {
455 pushBack_PtrArray(&d->packs, pack); 519 pushBack_PtrArray(&d->packs, pack);
456 } 520 }
@@ -491,13 +555,18 @@ void init_Fonts(const char *userDir) {
491void deinit_Fonts(void) { 555void deinit_Fonts(void) {
492 iFonts *d = &fonts_; 556 iFonts *d = &fonts_;
493 unloadFonts_Fonts_(d); 557 unloadFonts_Fonts_(d);
494 unloadFiles_Fonts_(d); 558 //unloadFiles_Fonts_(d);
559 iAssert(isEmpty_ObjectList(d->files));
495 deinit_PtrArray(&d->specOrder); 560 deinit_PtrArray(&d->specOrder);
496 deinit_PtrArray(&d->packs); 561 deinit_PtrArray(&d->packs);
497 deinit_PtrArray(&d->files); 562 iRelease(d->files);
498 deinit_String(&d->userDir); 563 deinit_String(&d->userDir);
499} 564}
500 565
566const iPtrArray *listPacks_Fonts(void) {
567 return &fonts_.packs;
568}
569
501const iFontSpec *findSpec_Fonts(const char *fontId) { 570const iFontSpec *findSpec_Fonts(const char *fontId) {
502 iFonts *d = &fonts_; 571 iFonts *d = &fonts_;
503 iConstForEach(PtrArray, i, &d->specOrder) { 572 iConstForEach(PtrArray, i, &d->specOrder) {
@@ -524,3 +593,73 @@ const iPtrArray *listSpecs_Fonts(iBool (*filterFunc)(const iFontSpec *)) {
524const iPtrArray *listSpecsByPriority_Fonts(void) { 593const iPtrArray *listSpecsByPriority_Fonts(void) {
525 return &fonts_.specOrder; 594 return &fonts_.specOrder;
526} 595}
596
597const iString *infoPage_Fonts(void) {
598 iFonts *d = &fonts_;
599 iString *str = collectNewCStr_String("# Fonts\n");
600 iPtrArray *specsByPack = collectNew_PtrArray();
601 setCopy_PtrArray(specsByPack, &d->specOrder);
602 sort_Array(specsByPack, cmpSourceAndPriority_FontSpecPtr_);
603 iString *currentSourcePath = collectNew_String();
604 iConstForEach(PtrArray, i, specsByPack) {
605 const iFontSpec *spec = i.ptr;
606 if (isEmpty_String(&spec->sourcePath)) {
607 continue; /* built-in font */
608 }
609 if (!equal_String(&spec->sourcePath, currentSourcePath)) {
610 appendFormat_String(str, "=> %s %s%s\n",
611 cstrCollect_String(makeFileUrl_String(&spec->sourcePath)),
612 endsWithCase_String(&spec->sourcePath, ".fontpack") ? "\U0001f520 " : "",
613 cstr_Rangecc(baseName_Path(&spec->sourcePath)));
614 set_String(currentSourcePath, &spec->sourcePath);
615 }
616 }
617 return str;
618}
619
620const iFontPack *findPack_Fonts(const iString *path) {
621 iFonts *d = &fonts_;
622 iConstForEach(PtrArray, i, &d->packs) {
623 const iFontPack *pack = i.ptr;
624 if (pack->loadPath && equal_String(pack->loadPath, path)) {
625 return pack;
626 }
627 }
628 return NULL;
629}
630
631iBool preloadLocalFontpackForPreview_Fonts(iGmDocument *doc) {
632 iBool wasLoaded = iFalse;
633 for (size_t linkId = 1; ; linkId++) {
634 const iString *linkUrl = linkUrl_GmDocument(doc, linkId);
635 if (!linkUrl) {
636 break; /* ran out of links */
637 }
638 const int linkFlags = linkFlags_GmDocument(doc, linkId);
639 if (linkFlags & fontpackFileExtension_GmLinkFlag &&
640 scheme_GmLinkFlag(linkFlags) == file_GmLinkScheme) {
641 iMediaId linkMedia = findMediaForLink_Media(media_GmDocument(doc), linkId, fontpack_MediaType);
642 if (linkMedia.type) {
643 continue; /* got this one already */
644 }
645 iString *filePath = localFilePathFromUrl_String(linkUrl);
646 iFile *f = new_File(filePath);
647 if (open_File(f, readOnly_FileMode)) {
648 iBlock *fontPackArchiveData = readAll_File(f);
649 setUrl_Media(media_GmDocument(doc), linkId, fontpack_MediaType, linkUrl);
650 setData_Media(media_GmDocument(doc),
651 linkId,
652 collectNewCStr_String(mimeType_FontPack),
653 fontPackArchiveData,
654 0);
655 delete_Block(fontPackArchiveData);
656 wasLoaded = iTrue;
657 }
658 iRelease(f);
659 }
660 }
661 return wasLoaded;
662}
663
664iDefineClass(FontFile)
665
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. */
30# include <hb.h> 30# include <hb.h>
31#endif 31#endif
32 32
33extern const char *mimeType_FontPack;
34
33/* Fontpacks are ZIP archives that contain a configuration file and one of more font 35/* Fontpacks are ZIP archives that contain a configuration file and one of more font
34files. The fontpack format is used instead of plain TTF/OTF because the text renderer 36files. The fontpack format is used instead of plain TTF/OTF because the text renderer
35uses additional metadata about each font. 37uses additional metadata about each font.
@@ -68,21 +70,13 @@ enum iFontStyle {
68 70
69float scale_FontSize (enum iFontSize size); 71float scale_FontSize (enum iFontSize size);
70 72
71iDeclareType(FontSpec) 73/*----------------------------------------------------------------------------------------------*/
72iDeclareTypeConstruction(FontSpec)
73 74
74enum iFontSpecFlags { 75iDeclareClass(FontFile)
75 override_FontSpecFlag = iBit(1), 76iDeclareObjectConstruction(FontFile)
76 monospace_FontSpecFlag = iBit(2), /* can be used in preformatted content */
77 auxiliary_FontSpecFlag = iBit(3), /* only used for looking up glyphs missing from other fonts */
78 arabic_FontSpecFlag = iBit(4),
79 fixNunitoKerning_FontSpecFlag = iBit(31), /* manual hardcoded kerning tweaks for Nunito */
80};
81
82iDeclareType(FontFile)
83iDeclareTypeConstruction(FontFile)
84 77
85struct Impl_FontFile { 78struct Impl_FontFile {
79 iObject object; /* reference-counted */
86 iString id; /* for detecting when the same file is used in many places */ 80 iString id; /* for detecting when the same file is used in many places */
87 enum iFontStyle style; 81 enum iFontStyle style;
88 iBlock sourceData; 82 iBlock sourceData;
@@ -107,9 +101,26 @@ uint8_t * rasterizeGlyph_FontFile(const iFontFile *, float xScale, float yScal
107void measureGlyph_FontFile (const iFontFile *, uint32_t glyphIndex, 101void measureGlyph_FontFile (const iFontFile *, uint32_t glyphIndex,
108 float xScale, float yScale, float xShift, 102 float xScale, float yScale, float xShift,
109 int *x0, int *y0, int *x1, int *y1); 103 int *x0, int *y0, int *x1, int *y1);
104
105/*----------------------------------------------------------------------------------------------*/
106
107/* FontSpec describes a typeface, combining multiple fonts into a group.
108 The user will be choosing FontSpecs instead of individual font files. */
109iDeclareType(FontSpec)
110iDeclareTypeConstruction(FontSpec)
111
112enum iFontSpecFlags {
113 override_FontSpecFlag = iBit(1),
114 monospace_FontSpecFlag = iBit(2), /* can be used in preformatted content */
115 auxiliary_FontSpecFlag = iBit(3), /* only used for looking up glyphs missing from other fonts */
116 arabic_FontSpecFlag = iBit(4),
117 fixNunitoKerning_FontSpecFlag = iBit(31), /* manual hardcoded kerning tweaks for Nunito */
118};
119
110struct Impl_FontSpec { 120struct Impl_FontSpec {
111 iString id; /* unique ID */ 121 iString id; /* unique ID */
112 iString name; /* human-readable label */ 122 iString name; /* human-readable label */
123 iString sourcePath; /* file where the path was loaded, could be a .fontpack */
113 int flags; 124 int flags;
114 int priority; 125 int priority;
115 float heightScale[2]; /* overall height scaling; ui, document */ 126 float heightScale[2]; /* overall height scaling; ui, document */
@@ -121,10 +132,38 @@ struct Impl_FontSpec {
121iLocalDef int scaleType_FontSpec(enum iFontSize sizeId) { 132iLocalDef int scaleType_FontSpec(enum iFontSize sizeId) {
122 return sizeId / contentRegular_FontSize; 133 return sizeId / contentRegular_FontSize;
123} 134}
124 135
136/*----------------------------------------------------------------------------------------------*/
137
138iDeclareType(FontPack)
139iDeclareTypeConstruction(FontPack)
140
141iDeclareType(FontPackId)
142
143struct Impl_FontPackId {
144 const iString *id;
145 int version;
146};
147
148void setReadOnly_FontPack (iFontPack *, iBool readOnly);
149void setStandalone_FontPack (iFontPack *, iBool standalone);
150void setLoadPath_FontPack (iFontPack *, const iString *path);
151iBool loadArchive_FontPack (iFontPack *, const iArchive *zip);
152
153iFontPackId id_FontPack (const iFontPack *);
154const iPtrArray * listSpecs_FontPack (const iFontPack *);
155iBool isReadOnly_FontPack (const iFontPack *);
156
157iDeclareType(GmDocument)
158
125void init_Fonts (const char *userDir); 159void init_Fonts (const char *userDir);
126void deinit_Fonts (void); 160void deinit_Fonts (void);
127 161
162const iFontPack * findPack_Fonts (const iString *path);
128const iFontSpec * findSpec_Fonts (const char *fontId); 163const iFontSpec * findSpec_Fonts (const char *fontId);
164const iPtrArray * listPacks_Fonts (void);
129const iPtrArray * listSpecs_Fonts (iBool (*filterFunc)(const iFontSpec *)); 165const iPtrArray * listSpecs_Fonts (iBool (*filterFunc)(const iFontSpec *));
130const iPtrArray * listSpecsByPriority_Fonts (void); 166const iPtrArray * listSpecsByPriority_Fonts (void);
167const iString * infoPage_Fonts (void);
168
169iBool 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) {
337 for (size_t linkId = 1; ; linkId++) { 337 for (size_t linkId = 1; ; linkId++) {
338 const iString *linkUrl = linkUrl_GmDocument(doc, linkId); 338 const iString *linkUrl = linkUrl_GmDocument(doc, linkId);
339 if (!linkUrl) break; 339 if (!linkUrl) break;
340 if (findLinkImage_Media(media_GmDocument(doc), linkId)) { 340 if (findLinkImage_Media(media_GmDocument(doc), linkId).type) {
341 continue; /* got this already */ 341 continue; /* got this already */
342 } 342 }
343 if (linkFlags_GmDocument(doc, linkId) & imageFileExtension_GmLinkFlag) { 343 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. */
27#include "ui/color.h" 27#include "ui/color.h"
28#include "ui/text.h" 28#include "ui/text.h"
29#include "ui/metrics.h" 29#include "ui/metrics.h"
30#include "ui/mediaui.h"
30#include "ui/window.h" 31#include "ui/window.h"
31#include "visited.h" 32#include "visited.h"
32#include "bookmarks.h" 33#include "bookmarks.h"
@@ -224,7 +225,7 @@ static iRangecc addLink_GmDocument_(iGmDocument *d, iRangecc line, iGmLinkId *li
224 setScheme_GmLink_(link, finger_GmLinkScheme); 225 setScheme_GmLink_(link, finger_GmLinkScheme);
225 } 226 }
226 else if (equalCase_Rangecc(parts.scheme, "file")) { 227 else if (equalCase_Rangecc(parts.scheme, "file")) {
227 setScheme_GmLink_(link, file_GmLinkScheme); 228 setScheme_GmLink_(link, file_GmLinkScheme);
228 } 229 }
229 else if (equalCase_Rangecc(parts.scheme, "data")) { 230 else if (equalCase_Rangecc(parts.scheme, "data")) {
230 setScheme_GmLink_(link, data_GmLinkScheme); 231 setScheme_GmLink_(link, data_GmLinkScheme);
@@ -251,6 +252,9 @@ static iRangecc addLink_GmDocument_(iGmDocument *d, iRangecc line, iGmLinkId *li
251 endsWithCase_String(path, ".mid") || endsWithCase_String(path, ".ogg")) { 252 endsWithCase_String(path, ".mid") || endsWithCase_String(path, ".ogg")) {
252 link->flags |= audioFileExtension_GmLinkFlag; 253 link->flags |= audioFileExtension_GmLinkFlag;
253 } 254 }
255 else if (endsWithCase_String(path, ".fontpack")) {
256 link->flags |= fontpackFileExtension_GmLinkFlag;
257 }
254 delete_String(path); 258 delete_String(path);
255 } 259 }
256 /* Check if visited. */ 260 /* Check if visited. */
@@ -503,7 +507,7 @@ static void doLayout_GmDocument_(iGmDocument *d) {
503 static const char *arrow = rightArrowhead_Icon; 507 static const char *arrow = rightArrowhead_Icon;
504 static const char *envelope = envelope_Icon; 508 static const char *envelope = envelope_Icon;
505 static const char *bullet = "\u2022"; 509 static const char *bullet = "\u2022";
506 static const char *folder = "\U0001f4c1"; 510 static const char *folder = file_Icon;
507 static const char *globe = globe_Icon; 511 static const char *globe = globe_Icon;
508 static const char *quote = "\u201c"; 512 static const char *quote = "\u201c";
509 static const char *magnifyingGlass = "\U0001f50d"; 513 static const char *magnifyingGlass = "\U0001f50d";
@@ -900,77 +904,77 @@ static void doLayout_GmDocument_(iGmDocument *d) {
900 ((iGmRun *) back_Array(&d->layout))->flags |= endOfLine_GmRunFlag; 904 ((iGmRun *) back_Array(&d->layout))->flags |= endOfLine_GmRunFlag;
901 /* Image or audio content. */ 905 /* Image or audio content. */
902 if (type == link_GmLineType) { 906 if (type == link_GmLineType) {
903 const iMediaId imageId = findLinkImage_Media(d->media, run.linkId); 907 /* TODO: Cleanup here? Move to a function of its own. */
904 const iMediaId audioId = !imageId ? findLinkAudio_Media(d->media, run.linkId) : 0; 908// enum iMediaType mediaType = none_MediaType;
905 const iMediaId downloadId = !imageId && !audioId ? findLinkDownload_Media(d->media, run.linkId) : 0; 909 const iMediaId media = findMediaForLink_Media(d->media, run.linkId, none_MediaType);
906 if (imageId) { 910 iGmMediaInfo info;
907 iGmMediaInfo img; 911 info_Media(d->media, media, &info);
908 imageInfo_Media(d->media, imageId, &img); 912 run.mediaType = media.type;
909 const iInt2 imgSize = imageSize_Media(d->media, imageId); 913 run.mediaId = media.id;
910 linkContentWasLaidOut_GmDocument_(d, &img, run.linkId); 914 run.text = iNullRange;
911 const int margin = lineHeight_Text(paragraph_FontId) / 2; 915 run.font = uiLabel_FontId;
916 run.color = 0;
917 const int margin = lineHeight_Text(paragraph_FontId) / 2;
918 if (media.type) {
912 pos.y += margin; 919 pos.y += margin;
913 run.bounds.pos = pos; 920 run.bounds.size.y = 0;
914 run.bounds.size.x = d->size.x;
915 const float aspect = (float) imgSize.y / (float) imgSize.x;
916 run.bounds.size.y = d->size.x * aspect;
917 /* Extend the image to full width, including outside margin, if the viewport
918 is narrow enough. */
919 if (isFullWidthImages) {
920 run.bounds.size.x += d->outsideMargin * 2;
921 run.bounds.size.y += d->outsideMargin * 2 * aspect;
922 run.bounds.pos.x -= d->outsideMargin;
923 }
924 run.visBounds = run.bounds;
925 const iInt2 maxSize = mulf_I2(imgSize, get_Window()->pixelRatio);
926 if (width_Rect(run.visBounds) > maxSize.x) {
927 /* Don't scale the image up. */
928 run.visBounds.size.y =
929 run.visBounds.size.y * maxSize.x / width_Rect(run.visBounds);
930 run.visBounds.size.x = maxSize.x;
931 run.visBounds.pos.x = run.bounds.size.x / 2 - width_Rect(run.visBounds) / 2;
932 run.bounds.size.y = run.visBounds.size.y;
933 }
934 run.text = iNullRange;
935 run.font = 0;
936 run.color = 0;
937 run.mediaType = image_GmRunMediaType;
938 run.mediaId = imageId;
939 pushBack_Array(&d->layout, &run);
940 pos.y += run.bounds.size.y + margin;
941 }
942 else if (audioId) {
943 iGmMediaInfo info;
944 audioInfo_Media(d->media, audioId, &info);
945 linkContentWasLaidOut_GmDocument_(d, &info, run.linkId); 921 linkContentWasLaidOut_GmDocument_(d, &info, run.linkId);
946 const int margin = lineHeight_Text(paragraph_FontId) / 2;
947 pos.y += margin;
948 run.bounds.pos = pos;
949 run.bounds.size.x = d->size.x;
950 run.bounds.size.y = lineHeight_Text(uiContent_FontId) + 3 * gap_UI;
951 run.visBounds = run.bounds;
952 run.text = iNullRange;
953 run.color = 0;
954 run.mediaType = audio_GmRunMediaType;
955 run.mediaId = audioId;
956 pushBack_Array(&d->layout, &run);
957 pos.y += run.bounds.size.y + margin;
958 } 922 }
959 else if (downloadId) { 923 switch (media.type) {
960 iGmMediaInfo info; 924 case image_MediaType: {
961 downloadInfo_Media(d->media, downloadId, &info); 925 const iInt2 imgSize = imageSize_Media(d->media, media);
962 linkContentWasLaidOut_GmDocument_(d, &info, run.linkId); 926 run.bounds.pos = pos;
963 const int margin = lineHeight_Text(paragraph_FontId) / 2; 927 run.bounds.size.x = d->size.x;
964 pos.y += margin; 928 const float aspect = (float) imgSize.y / (float) imgSize.x;
965 run.bounds.pos = pos; 929 run.bounds.size.y = d->size.x * aspect;
966 run.bounds.size.x = d->size.x; 930 /* Extend the image to full width, including outside margin, if the viewport
967 run.bounds.size.y = 2 * lineHeight_Text(uiContent_FontId) + 4 * gap_UI; 931 is narrow enough. */
968 run.visBounds = run.bounds; 932 if (isFullWidthImages) {
969 run.text = iNullRange; 933 run.bounds.size.x += d->outsideMargin * 2;
970 run.color = 0; 934 run.bounds.size.y += d->outsideMargin * 2 * aspect;
971 run.mediaType = download_GmRunMediaType; 935 run.bounds.pos.x -= d->outsideMargin;
972 run.mediaId = downloadId; 936 }
973 pushBack_Array(&d->layout, &run); 937 run.visBounds = run.bounds;
938 const iInt2 maxSize = mulf_I2(imgSize, get_Window()->pixelRatio);
939 if (width_Rect(run.visBounds) > maxSize.x) {
940 /* Don't scale the image up. */
941 run.visBounds.size.y =
942 run.visBounds.size.y * maxSize.x / width_Rect(run.visBounds);
943 run.visBounds.size.x = maxSize.x;
944 run.visBounds.pos.x = run.bounds.size.x / 2 - width_Rect(run.visBounds) / 2;
945 run.bounds.size.y = run.visBounds.size.y;
946 }
947 pushBack_Array(&d->layout, &run);
948 break;
949 }
950 case audio_MediaType: {
951 run.bounds.pos = pos;
952 run.bounds.size.x = d->size.x;
953 run.bounds.size.y = lineHeight_Text(uiContent_FontId) + 3 * gap_UI;
954 run.visBounds = run.bounds;
955 pushBack_Array(&d->layout, &run);
956 break;
957 }
958 case download_MediaType: {
959 run.bounds.pos = pos;
960 run.bounds.size.x = d->size.x;
961 run.bounds.size.y = 2 * lineHeight_Text(uiContent_FontId) + 4 * gap_UI;
962 run.visBounds = run.bounds;
963 pushBack_Array(&d->layout, &run);
964 break;
965 }
966 case fontpack_MediaType: {
967 run.bounds.pos = pos;
968 run.bounds.size.x = d->size.x;
969 run.bounds.size.y = height_FontpackUI(d->media, media.id, d->size.x);
970 run.visBounds = run.bounds;
971 pushBack_Array(&d->layout, &run);
972 break;
973 }
974 default:
975 break;
976 }
977 if (media.type && run.bounds.size.y) {
974 pos.y += run.bounds.size.y + margin; 978 pos.y += run.bounds.size.y + margin;
975 } 979 }
976 } 980 }
@@ -1057,21 +1061,6 @@ const iString *url_GmDocument(const iGmDocument *d) {
1057 return &d->url; 1061 return &d->url;
1058} 1062}
1059 1063
1060#if 0
1061void reset_GmDocument(iGmDocument *d) {
1062 clear_Media(d->media);
1063 clearLinks_GmDocument_(d);
1064 clear_Array(&d->layout);
1065 clear_Array(&d->headings);
1066 clear_Array(&d->preMeta);
1067 clear_String(&d->url);
1068 clear_String(&d->localHost);
1069 clear_String(&d->source);
1070 clear_String(&d->unormSource);
1071 d->themeSeed = 0;
1072}
1073#endif
1074
1075static void setDerivedThemeColors_(enum iGmDocumentTheme theme) { 1064static void setDerivedThemeColors_(enum iGmDocumentTheme theme) {
1076 set_Color(tmQuoteIcon_ColorId, 1065 set_Color(tmQuoteIcon_ColorId,
1077 mix_Color(get_Color(tmQuote_ColorId), get_Color(tmBackground_ColorId), 0.55f)); 1066 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 {
90 query_GmLinkFlag = iBit(14), /* Gopher query link */ 90 query_GmLinkFlag = iBit(14), /* Gopher query link */
91 iconFromLabel_GmLinkFlag = iBit(15), /* use an Emoji/special character from label */ 91 iconFromLabel_GmLinkFlag = iBit(15), /* use an Emoji/special character from label */
92 isOpen_GmLinkFlag = iBit(16), /* currently open in a tab */ 92 isOpen_GmLinkFlag = iBit(16), /* currently open in a tab */
93 fontpackFileExtension_GmLinkFlag = iBit(17),
93}; 94};
94 95
95iLocalDef enum iGmLinkScheme scheme_GmLinkFlag(int flags) { 96iLocalDef enum iGmLinkScheme scheme_GmLinkFlag(int flags) {
@@ -126,13 +127,6 @@ enum iGmRunFlags {
126 altText_GmRunFlag = iBit(8), 127 altText_GmRunFlag = iBit(8),
127}; 128};
128 129
129enum iGmRunMediaType {
130 none_GmRunMediaType,
131 image_GmRunMediaType,
132 audio_GmRunMediaType,
133 download_GmRunMediaType,
134};
135
136/* This structure is tightly packed because GmDocuments are mostly composed of 130/* This structure is tightly packed because GmDocuments are mostly composed of
137 a large number of GmRuns. */ 131 a large number of GmRuns. */
138struct Impl_GmRun { 132struct Impl_GmRun {
@@ -146,12 +140,16 @@ struct Impl_GmRun {
146 uint32_t color : 7; /* see max_ColorId */ 140 uint32_t color : 7; /* see max_ColorId */
147 141
148 uint32_t font : 10; 142 uint32_t font : 10;
149 uint32_t mediaType : 2; 143 uint32_t mediaType : 3;
150 uint32_t mediaId : 10; /* zero if not an image */ 144 uint32_t mediaId : 9; /* zero if not an image */
151 uint32_t preId : 10; /* preformatted block ID (sequential); merge with mediaId? */ 145 uint32_t preId : 10; /* preformatted block ID (sequential); merge with mediaId? */
152 }; 146 };
153}; 147};
154 148
149iLocalDef iMediaId mediaId_GmRun(const iGmRun *d) {
150 return (iMediaId){ .type = d->mediaType, .id = d->mediaId };
151}
152
155iDeclareType(GmRunRange) 153iDeclareType(GmRunRange)
156 154
157struct Impl_GmRunRange { 155struct 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) {
361 if (equalCase_Rangecc(path, "debug")) { 361 if (equalCase_Rangecc(path, "debug")) {
362 return utf8_String(debugInfo_App()); 362 return utf8_String(debugInfo_App());
363 } 363 }
364 if (equalCase_Rangecc(path, "fonts")) {
365 return utf8_String(infoPage_Fonts());
366 }
364 if (equalCase_Rangecc(path, "feeds")) { 367 if (equalCase_Rangecc(path, "feeds")) {
365 return utf8_String(entryListPage_Feeds()); 368 return utf8_String(entryListPage_Feeds());
366 } 369 }
@@ -710,8 +713,9 @@ void submit_GmRequest(iGmRequest *d) {
710 sort_Array(sortedInfo, (int (*)(const void *, const void *)) cmp_FileInfoPtr_); 713 sort_Array(sortedInfo, (int (*)(const void *, const void *)) cmp_FileInfoPtr_);
711 iForEach(PtrArray, s, sortedInfo) { 714 iForEach(PtrArray, s, sortedInfo) {
712 const iFileInfo *entry = s.ptr; 715 const iFileInfo *entry = s.ptr;
713 appendFormat_String(page, "=> %s %s%s\n", 716 appendFormat_String(page, "=> %s %s%s%s\n",
714 cstrCollect_String(makeFileUrl_String(path_FileInfo(entry))), 717 cstrCollect_String(makeFileUrl_String(path_FileInfo(entry))),
718 isDirectory_FileInfo(entry) ? folder_Icon " " : "",
715 cstr_Rangecc(baseName_Path(path_FileInfo(entry))), 719 cstr_Rangecc(baseName_Path(path_FileInfo(entry))),
716 isDirectory_FileInfo(entry) ? iPathSeparator : ""); 720 isDirectory_FileInfo(entry) ? iPathSeparator : "");
717 iRelease(entry); 721 iRelease(entry);
@@ -808,9 +812,10 @@ void submit_GmRequest(iGmRequest *d) {
808 const iString *subPath = e.value; 812 const iString *subPath = e.value;
809 iRangecc relSub = range_String(subPath); 813 iRangecc relSub = range_String(subPath);
810 relSub.start += size_String(entryPath); 814 relSub.start += size_String(entryPath);
811 appendFormat_String(page, "=> %s/%s %s\n", 815 appendFormat_String(page, "=> %s/%s %s%s\n",
812 cstr_String(&d->url), 816 cstr_String(&d->url),
813 cstr_String(withSpacesEncoded_String(collectNewRange_String(relSub))), 817 cstr_String(withSpacesEncoded_String(collectNewRange_String(relSub))),
818 endsWith_Rangecc(relSub, "/") ? folder_Icon " " : "",
814 cstr_Rangecc(relSub)); 819 cstr_Rangecc(relSub));
815 } 820 }
816 resp->statusCode = success_GmStatusCode; 821 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
21SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ 21SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22 22
23#include "gmutil.h" 23#include "gmutil.h"
24#include "fontpack.h"
24 25
25#include <the_Foundation/file.h> 26#include <the_Foundation/file.h>
26#include <the_Foundation/fileinfo.h> 27#include <the_Foundation/fileinfo.h>
@@ -511,7 +512,8 @@ const iString *findContainerArchive_Path(const iString *path) {
511 while (!isEmpty_String(path) && cmp_String(path, ".")) { 512 while (!isEmpty_String(path) && cmp_String(path, ".")) {
512 iString *dir = newRange_String(dirName_Path(path)); 513 iString *dir = newRange_String(dirName_Path(path));
513 if (endsWithCase_String(dir, ".zip") || 514 if (endsWithCase_String(dir, ".zip") ||
514 endsWithCase_String(dir, ".gpub")) { 515 endsWithCase_String(dir, ".gpub") ||
516 endsWithCase_String(dir, ".fontpack")) {
515 iEndCollect(); 517 iEndCollect();
516 return collect_String(dir); 518 return collect_String(dir);
517 } 519 }
@@ -534,6 +536,9 @@ const char *mediaTypeFromFileExtension_String(const iString *d) {
534 else if (endsWithCase_String(d, ".gpub")) { 536 else if (endsWithCase_String(d, ".gpub")) {
535 return "application/gpub+zip"; 537 return "application/gpub+zip";
536 } 538 }
539 else if (endsWithCase_String(d, ".fontpack")) {
540 return mimeType_FontPack;
541 }
537 else if (endsWithCase_String(d, ".xml")) { 542 else if (endsWithCase_String(d, ".xml")) {
538 return "text/xml"; 543 return "text/xml";
539 } 544 }
@@ -562,6 +567,7 @@ const char *mediaTypeFromFileExtension_String(const iString *d) {
562 return "audio/midi"; 567 return "audio/midi";
563 } 568 }
564 else if (endsWithCase_String(d, ".txt") || 569 else if (endsWithCase_String(d, ".txt") ||
570 endsWithCase_String(d, ".ini") ||
565 endsWithCase_String(d, ".md") || 571 endsWithCase_String(d, ".md") ||
566 endsWithCase_String(d, ".c") || 572 endsWithCase_String(d, ".c") ||
567 endsWithCase_String(d, ".h") || 573 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. */
36 36
37#include <the_Foundation/file.h> 37#include <the_Foundation/file.h>
38#include <the_Foundation/ptrarray.h> 38#include <the_Foundation/ptrarray.h>
39#include <the_Foundation/stringlist.h>
39#include <SDL_hints.h> 40#include <SDL_hints.h>
40#include <SDL_render.h> 41#include <SDL_render.h>
41#include <SDL_timer.h> 42#include <SDL_timer.h>
@@ -287,45 +288,112 @@ iDefineTypeConstruction(GmDownload)
287 288
288/*----------------------------------------------------------------------------------------------*/ 289/*----------------------------------------------------------------------------------------------*/
289 290
291iDeclareType(GmFontpack)
292
293struct Impl_GmFontpack {
294 iGmMediaProps props;
295 iString packId;
296 iFontpackMediaInfo info;
297 /* TODO: Font preview images? */
298};
299
300void init_GmFontpack(iGmFontpack *d) {
301 init_GmMediaProps_(&d->props);
302 init_String(&d->packId);
303 iZap(d->info);
304 d->info.names = new_StringList();
305}
306
307void deinit_GmFontpack(iGmFontpack *d) {
308 iRelease(d->info.names);
309 deinit_String(&d->packId);
310 deinit_GmMediaProps_(&d->props);
311}
312
313static void loadData_GmFontpack_(iGmFontpack *d, const iBlock *data) {
314 const iString *loadPath = collect_String(localFilePathFromUrl_String(&d->props.url));
315 const iFontPack *pack = findPack_Fonts(loadPath);
316 d->info.isValid = d->info.isInstalled = pack != NULL;
317 d->info.isReadOnly = iFalse;
318 if (!pack) {
319 /* Let's load it now temporarily and see what's inside. */
320 iArchive *zip = new_Archive();
321 if (openData_Archive(zip, data)) {
322 iFontPack *fp = collect_FontPack(new_FontPack());
323 setLoadPath_FontPack(fp, loadPath);
324 setStandalone_FontPack(fp, iTrue);
325 if (loadArchive_FontPack(fp, zip)) {
326 d->info.isValid = iTrue;
327 pack = fp;
328 }
329 }
330 iRelease(zip);
331 }
332 if (pack) {
333 set_String(&d->packId, id_FontPack(pack).id);
334 d->info.packId.id = &d->packId; /* we own this String */
335 d->info.packId.version = id_FontPack(pack).version;
336 d->info.isReadOnly = isReadOnly_FontPack(pack);
337 }
338 iPtrSet *unique = new_PtrSet();
339 iConstForEach(PtrArray, i, listSpecs_FontPack(pack)) {
340 const iFontSpec *spec = i.ptr;
341 pushBack_StringList(d->info.names, &spec->name);
342 iForIndices(j, spec->styles) {
343 insert_PtrSet(unique, spec->styles[j]);
344 }
345 }
346 iConstForEach(PtrSet, j, unique) {
347 d->info.sizeInBytes += size_Block(&((const iFontFile *) *j.value)->sourceData);
348 }
349 delete_PtrSet(unique);
350}
351
352iDefineTypeConstruction(GmFontpack)
353
354/*----------------------------------------------------------------------------------------------*/
355
290struct Impl_Media { 356struct Impl_Media {
291 iPtrArray images; 357 iPtrArray items[max_MediaType];
292 iPtrArray audio; 358 /* TODO: Add a hash to quickly look up a link's media. */
293 iPtrArray downloads;
294}; 359};
295 360
296iDefineTypeConstruction(Media) 361iDefineTypeConstruction(Media)
297 362
298void init_Media(iMedia *d) { 363void init_Media(iMedia *d) {
299 init_PtrArray(&d->images); 364 iForIndices(i, d->items) {
300 init_PtrArray(&d->audio); 365 init_PtrArray(&d->items[i]);
301 init_PtrArray(&d->downloads); 366 }
302} 367}
303 368
304void deinit_Media(iMedia *d) { 369void deinit_Media(iMedia *d) {
305 clear_Media(d); 370 clear_Media(d);
306 deinit_PtrArray(&d->downloads); 371 iForIndices(i, d->items) {
307 deinit_PtrArray(&d->audio); 372 deinit_PtrArray(&d->items[i]);
308 deinit_PtrArray(&d->images); 373 }
309} 374}
310 375
311void clear_Media(iMedia *d) { 376void clear_Media(iMedia *d) {
312 iForEach(PtrArray, i, &d->images) { 377 iForEach(PtrArray, i, &d->items[image_MediaType]) {
313 deinit_GmImage(i.ptr); 378 deinit_GmImage(i.ptr);
314 } 379 }
315 clear_PtrArray(&d->images); 380 iForEach(PtrArray, a, &d->items[audio_MediaType]) {
316 iForEach(PtrArray, a, &d->audio) {
317 deinit_GmAudio(a.ptr); 381 deinit_GmAudio(a.ptr);
318 } 382 }
319 clear_PtrArray(&d->audio); 383 iForEach(PtrArray, n, &d->items[download_MediaType]) {
320 iForEach(PtrArray, n, &d->downloads) {
321 deinit_GmDownload(n.ptr); 384 deinit_GmDownload(n.ptr);
322 } 385 }
323 clear_PtrArray(&d->downloads); 386 iForEach(PtrArray, f, &d->items[fontpack_MediaType]) {
387 deinit_GmFontpack(f.ptr);
388 }
389 iForIndices(type, d->items) {
390 clear_PtrArray(&d->items[type]);
391 }
324} 392}
325 393
326size_t memorySize_Media(const iMedia *d) { 394size_t memorySize_Media(const iMedia *d) {
327 size_t memSize = 0; 395 size_t memSize = 0;
328 iConstForEach(PtrArray, i, &d->images) { 396 iConstForEach(PtrArray, i, &d->items[image_MediaType]) {
329 const iGmImage *img = i.ptr; 397 const iGmImage *img = i.ptr;
330 if (img->texture) { 398 if (img->texture) {
331 const iInt2 texSize = size_SDLTexture(img->texture); 399 const iInt2 texSize = size_SDLTexture(img->texture);
@@ -335,34 +403,49 @@ size_t memorySize_Media(const iMedia *d) {
335 memSize += size_Block(&img->partialData); 403 memSize += size_Block(&img->partialData);
336 } 404 }
337 } 405 }
338 iConstForEach(PtrArray, a, &d->audio) { 406 iConstForEach(PtrArray, a, &d->items[audio_MediaType]) {
339 const iGmAudio *audio = a.ptr; 407 const iGmAudio *audio = a.ptr;
340 if (audio->player) { 408 if (audio->player) {
341 memSize += sourceDataSize_Player(audio->player); 409 memSize += sourceDataSize_Player(audio->player);
342 } 410 }
343 } 411 }
344 iConstForEach(PtrArray, n, &d->downloads) { 412 iConstForEach(PtrArray, n, &d->items[download_MediaType]) {
345 const iGmDownload *down = n.ptr; 413 const iGmDownload *down = n.ptr;
346 memSize += down->numBytes; 414 memSize += down->numBytes;
347 } 415 }
348 return memSize; 416 return memSize;
349} 417}
350 418
351iBool setDownloadUrl_Media(iMedia *d, iGmLinkId linkId, const iString *url) { 419iBool setUrl_Media(iMedia *d, iGmLinkId linkId, enum iMediaType mediaType, const iString *url) {
352 iGmDownload *dl = NULL; 420 iMediaId existing = findMediaForLink_Media(d, linkId, mediaType);
353 iMediaId existing = findLinkDownload_Media(d, linkId); 421 const iBool isNew = !existing.id;
354 iBool isNew = iFalse; 422 iGmMediaProps *props = NULL;
355 if (!existing) { 423 if (mediaType == download_MediaType) {
356 isNew = iTrue; 424 iGmDownload *dl = NULL;
357 dl = new_GmDownload(); 425 if (isNew) {
358 dl->props.linkId = linkId; 426 dl = new_GmDownload();
359 dl->props.isPermanent = iTrue; 427 pushBack_PtrArray(&d->items[download_MediaType], dl);
360 set_String(&dl->props.url, url); 428 }
361 pushBack_PtrArray(&d->downloads, dl); 429 else {
430 dl = at_PtrArray(&d->items[download_MediaType], index_MediaId(existing));
431 }
432 props = &dl->props;
362 } 433 }
363 else { 434 else if (mediaType == fontpack_MediaType) {
364 iGmDownload *dl = at_PtrArray(&d->downloads, existing - 1); 435 iGmFontpack *fp = NULL;
365 set_String(&dl->props.url, url); 436 if (isNew) {
437 fp = new_GmFontpack();
438 pushBack_PtrArray(&d->items[fontpack_MediaType], fp);
439 }
440 else {
441 fp = at_PtrArray(&d->items[fontpack_MediaType], index_MediaId(existing));
442 }
443 props = &fp->props;
444 }
445 if (props) {
446 props->linkId = linkId;
447 props->isPermanent = iTrue;
448 set_String(&props->url, url);
366 } 449 }
367 return isNew; 450 return isNew;
368} 451}
@@ -372,16 +455,17 @@ iBool setData_Media(iMedia *d, iGmLinkId linkId, const iString *mime, const iBlo
372 const iBool isPartial = (flags & partialData_MediaFlag) != 0; 455 const iBool isPartial = (flags & partialData_MediaFlag) != 0;
373 const iBool allowHide = (flags & allowHide_MediaFlag) != 0; 456 const iBool allowHide = (flags & allowHide_MediaFlag) != 0;
374 const iBool isDeleting = (!mime || !data); 457 const iBool isDeleting = (!mime || !data);
375 iMediaId existing = findLinkImage_Media(d, linkId); 458 iMediaId existing = findMediaForLink_Media(d, linkId, none_MediaType);// findLinkImage_Media(d, linkId);
459 const size_t existingIndex = index_MediaId(existing);
376 iBool isNew = iFalse; 460 iBool isNew = iFalse;
377 if (existing) { 461 if (existing.type == image_MediaType) {
378 iGmImage *img; 462 iGmImage *img;
379 if (isDeleting) { 463 if (isDeleting) {
380 take_PtrArray(&d->images, existing - 1, (void **) &img); 464 take_PtrArray(&d->items[image_MediaType], existingIndex, (void **) &img);
381 delete_GmImage(img); 465 delete_GmImage(img);
382 } 466 }
383 else { 467 else {
384 img = at_PtrArray(&d->images, existing - 1); 468 img = at_PtrArray(&d->items[image_MediaType], existingIndex);
385 iAssert(equal_String(&img->props.mime, mime)); /* MIME cannot change */ 469 iAssert(equal_String(&img->props.mime, mime)); /* MIME cannot change */
386 set_Block(&img->partialData, data); 470 set_Block(&img->partialData, data);
387 if (!isPartial) { 471 if (!isPartial) {
@@ -389,14 +473,14 @@ iBool setData_Media(iMedia *d, iGmLinkId linkId, const iString *mime, const iBlo
389 } 473 }
390 } 474 }
391 } 475 }
392 else if ((existing = findLinkAudio_Media(d, linkId)) != 0) { 476 else if (existing.type == audio_MediaType) {
393 iGmAudio *audio; 477 iGmAudio *audio;
394 if (isDeleting) { 478 if (isDeleting) {
395 take_PtrArray(&d->audio, existing - 1, (void **) &audio); 479 take_PtrArray(&d->items[audio_MediaType], existingIndex, (void **) &audio);
396 delete_GmAudio(audio); 480 delete_GmAudio(audio);
397 } 481 }
398 else { 482 else {
399 audio = at_PtrArray(&d->audio, existing - 1); 483 audio = at_PtrArray(&d->items[audio_MediaType], existingIndex);
400 iAssert(equal_String(&audio->props.mime, mime)); /* MIME cannot change */ 484 iAssert(equal_String(&audio->props.mime, mime)); /* MIME cannot change */
401 updateSourceData_Player(audio->player, mime, data, append_PlayerUpdate); 485 updateSourceData_Player(audio->player, mime, data, append_PlayerUpdate);
402 if (!isPartial) { 486 if (!isPartial) {
@@ -408,14 +492,14 @@ iBool setData_Media(iMedia *d, iGmLinkId linkId, const iString *mime, const iBlo
408 } 492 }
409 } 493 }
410 } 494 }
411 else if ((existing = findLinkDownload_Media(d, linkId)) != 0) { 495 else if (existing.type == download_MediaType) {
412 iGmDownload *dl; 496 iGmDownload *dl;
413 if (isDeleting) { 497 if (isDeleting) {
414 take_PtrArray(&d->downloads, existing - 1, (void **) &dl); 498 take_PtrArray(&d->items[download_MediaType], existingIndex, (void **) &dl);
415 delete_GmDownload(dl); 499 delete_GmDownload(dl);
416 } 500 }
417 else { 501 else {
418 dl = at_PtrArray(&d->downloads, existing - 1); 502 dl = at_PtrArray(&d->items[download_MediaType], existingIndex);
419 if (isEmpty_String(&dl->props.mime)) { 503 if (isEmpty_String(&dl->props.mime)) {
420 set_String(&dl->props.mime, mime); 504 set_String(&dl->props.mime, mime);
421 } 505 }
@@ -428,6 +512,21 @@ iBool setData_Media(iMedia *d, iGmLinkId linkId, const iString *mime, const iBlo
428 } 512 }
429 } 513 }
430 } 514 }
515 else if (existing.type == fontpack_MediaType) {
516 iGmFontpack *fp;
517 if (isDeleting) {
518 take_PtrArray(&d->items[fontpack_MediaType], existingIndex, (void **) &fp);
519 delete_GmFontpack(fp);
520 }
521 else {
522 iAssert(!isPartial);
523 fp = at_PtrArray(&d->items[fontpack_MediaType], existingIndex);
524 if (isEmpty_String(&fp->props.mime)) {
525 set_String(&fp->props.mime, mime);
526 }
527 loadData_GmFontpack_(fp, data);
528 }
529 }
431 else if (!isDeleting) { 530 else if (!isDeleting) {
432 if (startsWith_String(mime, "image/")) { 531 if (startsWith_String(mime, "image/")) {
433 /* Copy the image to a texture. */ 532 /* Copy the image to a texture. */
@@ -435,7 +534,7 @@ iBool setData_Media(iMedia *d, iGmLinkId linkId, const iString *mime, const iBlo
435 img->props.linkId = linkId; /* TODO: use a hash? */ 534 img->props.linkId = linkId; /* TODO: use a hash? */
436 img->props.isPermanent = !allowHide; 535 img->props.isPermanent = !allowHide;
437 set_String(&img->props.mime, mime); 536 set_String(&img->props.mime, mime);
438 pushBack_PtrArray(&d->images, img); 537 pushBack_PtrArray(&d->items[image_MediaType], img);
439 if (!isPartial) { 538 if (!isPartial) {
440 makeTexture_GmImage(img); 539 makeTexture_GmImage(img);
441 } 540 }
@@ -450,7 +549,7 @@ iBool setData_Media(iMedia *d, iGmLinkId linkId, const iString *mime, const iBlo
450 if (!isPartial) { 549 if (!isPartial) {
451 updateSourceData_Player(audio->player, NULL, NULL, complete_PlayerUpdate); 550 updateSourceData_Player(audio->player, NULL, NULL, complete_PlayerUpdate);
452 } 551 }
453 pushBack_PtrArray(&d->audio, audio); 552 pushBack_PtrArray(&d->items[audio_MediaType], audio);
454 /* Start playing right away. */ 553 /* Start playing right away. */
455 start_Player(audio->player); 554 start_Player(audio->player);
456 postCommandf_App("media.player.started player:%p", audio->player); 555 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
460 return isNew; 559 return isNew;
461} 560}
462 561
463iMediaId findLinkImage_Media(const iMedia *d, iGmLinkId linkId) { 562static iMediaId findMediaPtr_Media_(const iPtrArray *items, enum iMediaType mediaType, iGmLinkId linkId) {
464 /* TODO: use a hash */ 563 iConstForEach(PtrArray, i, items) {
465 iConstForEach(PtrArray, i, &d->images) { 564 const iGmMediaProps *props = i.ptr;
466 const iGmImage *img = i.ptr; 565 if (props->linkId == linkId) {
467 if (img->props.linkId == linkId) { 566 return (iMediaId){
468 return index_PtrArrayConstIterator(&i) + 1; 567 .type = mediaType,
568 .id = index_PtrArrayConstIterator(&i) + 1
569 };
469 } 570 }
470 } 571 }
471 return 0; 572 return iInvalidMediaId;
472}
473
474size_t numAudio_Media(const iMedia *d) {
475 return size_PtrArray(&d->audio);
476} 573}
477 574
478iMediaId findLinkAudio_Media(const iMedia *d, iGmLinkId linkId) { 575iMediaId findMediaForLink_Media(const iMedia *d, iGmLinkId linkId, enum iMediaType mediaType) {
479 /* TODO: use a hash */ 576 /* TODO: Use hashes, this will get very slow if there is a large number of media items. */
480 iConstForEach(PtrArray, i, &d->audio) { 577 iMediaId mid;
481 const iGmAudio *audio = i.ptr; 578 for (int i = 0; i < max_MediaType; i++) {
482 if (audio->props.linkId == linkId) { 579 if (mediaType == i || !mediaType) {
483 return index_PtrArrayConstIterator(&i) + 1; 580 mid = findMediaPtr_Media_(&d->items[i], i, linkId);
581 if (mid.type) {
582 return mid;
583 }
484 } 584 }
485 } 585 }
486 return 0; 586 return iInvalidMediaId;
487} 587}
488 588
489iMediaId findLinkDownload_Media(const iMedia *d, uint16_t linkId) { 589size_t numAudio_Media(const iMedia *d) {
490 iConstForEach(PtrArray, i, &d->downloads) { 590 return size_PtrArray(&d->items[audio_MediaType]);
491 const iGmDownload *dl = i.ptr;
492 if (dl->props.linkId == linkId) {
493 return index_PtrArrayConstIterator(&i) + 1;
494 }
495 }
496 return 0;
497} 591}
498 592
499iInt2 imageSize_Media(const iMedia *d, iMediaId imageId) { 593iInt2 imageSize_Media(const iMedia *d, iMediaId imageId) {
500 if (imageId > 0 && imageId <= size_PtrArray(&d->images)) { 594 iAssert(imageId.type == image_MediaType);
501 const iGmImage *img = constAt_PtrArray(&d->images, imageId - 1); 595 const size_t index = index_MediaId(imageId);
596 if (index < size_PtrArray(&d->items[image_MediaType])) {
597 const iGmImage *img = constAt_PtrArray(&d->items[image_MediaType], index);
502 return img->size; 598 return img->size;
503 } 599 }
504 return zero_I2(); 600 return zero_I2();
505} 601}
506 602
507SDL_Texture *imageTexture_Media(const iMedia *d, uint16_t imageId) { 603SDL_Texture *imageTexture_Media(const iMedia *d, iMediaId imageId) {
508 if (imageId > 0 && imageId <= size_PtrArray(&d->images)) { 604 iAssert(imageId.type == image_MediaType);
509 const iGmImage *img = constAt_PtrArray(&d->images, imageId - 1); 605 const size_t index = index_MediaId(imageId);
606 if (index < size_PtrArray(&d->items[image_MediaType])) {
607 const iGmImage *img = constAt_PtrArray(&d->items[image_MediaType], index);
510 return img->texture; 608 return img->texture;
511 } 609 }
512 return NULL; 610 return NULL;
513} 611}
514 612
515iBool imageInfo_Media(const iMedia *d, iMediaId imageId, iGmMediaInfo *info_out) { 613iBool info_Media(const iMedia *d, iMediaId mediaId, iGmMediaInfo *info_out) {
516 if (imageId > 0 && imageId <= size_PtrArray(&d->images)) { 614 /* TODO: Use a hash. */
517 const iGmImage *img = constAt_PtrArray(&d->images, imageId - 1); 615 const size_t index = index_MediaId(mediaId);
518 info_out->numBytes = img->numBytes; 616 switch (mediaId.type) {
519 info_out->type = cstr_String(&img->props.mime); 617 case image_MediaType:
520 info_out->isPermanent = img->props.isPermanent; 618 if (index < size_PtrArray(&d->items[image_MediaType])) {
521 return iTrue; 619 const iGmImage *img = constAt_PtrArray(&d->items[image_MediaType], index);
620 info_out->numBytes = img->numBytes;
621 info_out->type = cstr_String(&img->props.mime);
622 info_out->isPermanent = img->props.isPermanent;
623 return iTrue;
624 }
625 break;
626 case audio_MediaType:
627 if (index < size_PtrArray(&d->items[audio_MediaType])) {
628 const iGmAudio *audio = constAt_PtrArray(&d->items[audio_MediaType], index);
629 info_out->type = cstr_String(&audio->props.mime);
630 info_out->isPermanent = audio->props.isPermanent;
631 return iTrue;
632 }
633 break;
634 case download_MediaType:
635 if (index < size_PtrArray(&d->items[download_MediaType])) {
636 const iGmDownload *dl = constAt_PtrArray(&d->items[download_MediaType], index);
637 info_out->type = cstr_String(&dl->props.mime);
638 info_out->isPermanent = dl->props.isPermanent;
639 info_out->numBytes = dl->numBytes;
640 return iTrue;
641 }
642 break;
643 case fontpack_MediaType:
644 /* TODO */
645 break;
646 default:
647 break;
522 } 648 }
523 iZap(*info_out); 649 iZap(*info_out);
524 return iFalse; 650 return iFalse;
525} 651}
526 652
527iPlayer *audioData_Media(const iMedia *d, iMediaId audioId) { 653iPlayer *audioData_Media(const iMedia *d, iMediaId audioId) {
528 if (audioId > 0 && audioId <= size_PtrArray(&d->audio)) { 654 iAssert(audioId.type == audio_MediaType);
529 const iGmAudio *audio = constAt_PtrArray(&d->audio, audioId - 1); 655 const size_t index = index_MediaId(audioId);
656 if (index < size_PtrArray(&d->items[audio_MediaType])) {
657 const iGmAudio *audio = constAt_PtrArray(&d->items[audio_MediaType], index);
530 return audio->player; 658 return audio->player;
531 } 659 }
532 return NULL; 660 return NULL;
533} 661}
534 662
535iBool audioInfo_Media(const iMedia *d, iMediaId audioId, iGmMediaInfo *info_out) {
536 if (audioId > 0 && audioId <= size_PtrArray(&d->audio)) {
537 const iGmAudio *audio = constAt_PtrArray(&d->audio, audioId - 1);
538 info_out->type = cstr_String(&audio->props.mime);
539 info_out->isPermanent = audio->props.isPermanent;
540 return iTrue;
541 }
542 iZap(*info_out);
543 return iFalse;
544}
545
546iPlayer *audioPlayer_Media(const iMedia *d, iMediaId audioId) { 663iPlayer *audioPlayer_Media(const iMedia *d, iMediaId audioId) {
547 if (audioId > 0 && audioId <= size_PtrArray(&d->audio)) { 664 iAssert(audioId.type == audio_MediaType);
548 const iGmAudio *audio = constAt_PtrArray(&d->audio, audioId - 1); 665 const size_t index = index_MediaId(audioId);
666 if (index < size_PtrArray(&d->items[audio_MediaType])) {
667 const iGmAudio *audio = constAt_PtrArray(&d->items[audio_MediaType], index);
549 return audio->player; 668 return audio->player;
550 } 669 }
551 return NULL; 670 return NULL;
552} 671}
553 672
554void pauseAllPlayers_Media(const iMedia *d, iBool setPaused) { 673void pauseAllPlayers_Media(const iMedia *d, iBool setPaused) {
555 for (size_t i = 0; i < size_PtrArray(&d->audio); ++i) { 674 for (size_t i = 0; i < size_PtrArray(&d->items[audio_MediaType]); ++i) {
556 const iGmAudio *audio = constAt_PtrArray(&d->audio, i); 675 const iGmAudio *audio = constAt_PtrArray(&d->items[audio_MediaType], i);
557 if (audio->player) { 676 if (audio->player) {
558 setPaused_Player(audio->player, setPaused); 677 setPaused_Player(audio->player, setPaused);
559 } 678 }
560 } 679 }
561} 680}
562 681
563iBool downloadInfo_Media(const iMedia *d, iMediaId downloadId, iGmMediaInfo *info_out) {
564 if (downloadId > 0 && downloadId <= size_PtrArray(&d->downloads)) {
565 const iGmDownload *dl = constAt_PtrArray(&d->downloads, downloadId - 1);
566 info_out->type = cstr_String(&dl->props.mime);
567 info_out->isPermanent = dl->props.isPermanent;
568 info_out->numBytes = dl->numBytes;
569 return iTrue;
570 }
571 iZap(*info_out);
572 return iFalse;
573}
574
575void downloadStats_Media(const iMedia *d, iMediaId downloadId, const iString **path_out, 682void downloadStats_Media(const iMedia *d, iMediaId downloadId, const iString **path_out,
576 float *bytesPerSecond_out, iBool *isFinished_out) { 683 float *bytesPerSecond_out, iBool *isFinished_out) {
577 *path_out = NULL; 684 iAssert(downloadId.type == download_MediaType);
685 *path_out = NULL;
578 *bytesPerSecond_out = 0.0f; 686 *bytesPerSecond_out = 0.0f;
579 *isFinished_out = iFalse; 687 *isFinished_out = iFalse;
580 if (downloadId > 0 && downloadId <= size_PtrArray(&d->downloads)) { 688 const size_t index = index_MediaId(downloadId);
581 const iGmDownload *dl = constAt_PtrArray(&d->downloads, downloadId - 1); 689 if (index < size_PtrArray(&d->items[download_MediaType])) {
690 const iGmDownload *dl = constAt_PtrArray(&d->items[download_MediaType], index);
582 if (dl->path) { 691 if (dl->path) {
583 *path_out = dl->path; 692 *path_out = dl->path;
584 } 693 }
@@ -587,6 +696,16 @@ void downloadStats_Media(const iMedia *d, iMediaId downloadId, const iString **p
587 } 696 }
588} 697}
589 698
699void fontpackInfo_Media(const iMedia *d, iMediaId fontpackId, iFontpackMediaInfo *info_out) {
700 iAssert(fontpackId.type == fontpack_MediaType);
701 iZap(*info_out);
702 const size_t index = index_MediaId(fontpackId);
703 if (index < size_PtrArray(&d->items[fontpack_MediaType])) {
704 const iGmFontpack *fp = constAt_PtrArray(&d->items[fontpack_MediaType], index);
705 *info_out = fp->info;
706 }
707}
708
590/*----------------------------------------------------------------------------------------------*/ 709/*----------------------------------------------------------------------------------------------*/
591 710
592static void updated_MediaRequest_(iAnyObject *obj) { 711static 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. */
22 22
23#pragma once 23#pragma once
24 24
25#include "fontpack.h"
26
25#include <the_Foundation/block.h> 27#include <the_Foundation/block.h>
26#include <the_Foundation/string.h> 28#include <the_Foundation/string.h>
27#include <the_Foundation/vec2.h> 29#include <the_Foundation/vec2.h>
28#include <SDL_render.h> 30#include <SDL_render.h>
29 31
30typedef uint16_t iMediaId;
31
32iDeclareType(Player) 32iDeclareType(Player)
33iDeclareType(GmMediaInfo) 33iDeclareType(GmMediaInfo)
34 34
@@ -38,6 +38,7 @@ struct Impl_GmMediaInfo {
38 iBool isPermanent; 38 iBool isPermanent;
39}; 39};
40 40
41iDeclareType(MediaId)
41iDeclareType(Media) 42iDeclareType(Media)
42iDeclareTypeConstruction(Media) 43iDeclareTypeConstruction(Media)
43 44
@@ -46,28 +47,81 @@ enum iMediaFlags {
46 partialData_MediaFlag = iBit(2), 47 partialData_MediaFlag = iBit(2),
47}; 48};
48 49
49void clear_Media (iMedia *); 50enum iMediaType { /* Note: There is a limited number of bits for these; see GmRun below. */
50iBool setDownloadUrl_Media (iMedia *, uint16_t linkId, const iString *url); 51 none_MediaType,
51iBool setData_Media (iMedia *, uint16_t linkId, const iString *mime, const iBlock *data, int flags); 52 image_MediaType,
52 53 //animatedImage_MediaType, /* TODO */
53size_t memorySize_Media (const iMedia *); 54 audio_MediaType,
55 download_MediaType,
56 fontpack_MediaType,
57 max_MediaType
58};
54 59
55iMediaId findLinkImage_Media (const iMedia *, uint16_t linkId); 60struct Impl_MediaId {
56iBool imageInfo_Media (const iMedia *, iMediaId imageId, iGmMediaInfo *info_out); 61 enum iMediaType type;
57iInt2 imageSize_Media (const iMedia *, iMediaId imageId); 62 uint16_t id; /* see GmRun for actually used number of bits */
58SDL_Texture * imageTexture_Media (const iMedia *, iMediaId imageId); 63};
59 64
60size_t numAudio_Media (const iMedia *); 65iLocalDef size_t index_MediaId(const iMediaId mediaId) {
61iMediaId findLinkAudio_Media (const iMedia *, uint16_t linkId); 66 return (size_t) mediaId.id - 1;
62iBool audioInfo_Media (const iMedia *, iMediaId audioId, iGmMediaInfo *info_out); 67}
63iPlayer * audioPlayer_Media (const iMedia *, iMediaId audioId); 68
64void pauseAllPlayers_Media(const iMedia *, iBool setPaused); 69#define iInvalidMediaId (iMediaId){ none_MediaType, 0 }
70
71void clear_Media (iMedia *);
72iBool setUrl_Media (iMedia *, uint16_t linkId, enum iMediaType mediaType, const iString *url);
73iBool setData_Media (iMedia *, uint16_t linkId, const iString *mime, const iBlock *data, int flags);
74
75size_t memorySize_Media (const iMedia *);
76iMediaId findMediaForLink_Media (const iMedia *, uint16_t linkId, enum iMediaType mediaType);
77
78iMediaId id_Media (const iMedia *, uint16_t linkId, enum iMediaType type);
79iBool info_Media (const iMedia *, iMediaId mediaId, iGmMediaInfo *info_out);
80
81iLocalDef iMediaId findLinkImage_Media(const iMedia *d, uint16_t linkId) {
82 return findMediaForLink_Media(d, linkId, image_MediaType);
83}
84iLocalDef iMediaId findLinkAudio_Media (const iMedia *d, uint16_t linkId) {
85 return findMediaForLink_Media(d, linkId, audio_MediaType);
86}
87iLocalDef iMediaId findLinkDownload_Media(const iMedia *d, uint16_t linkId) {
88 return findMediaForLink_Media(d, linkId, download_MediaType);
89}
90
91iLocalDef iBool imageInfo_Media(const iMedia *d, uint16_t mediaId, iGmMediaInfo *info_out) {
92 return info_Media(d, (iMediaId){ image_MediaType, mediaId }, info_out);
93}
94iLocalDef iBool audioInfo_Media(const iMedia *d, uint16_t mediaId, iGmMediaInfo *info_out) {
95 return info_Media(d, (iMediaId){ audio_MediaType, mediaId }, info_out);
96}
97iLocalDef iBool downloadInfo_Media(const iMedia *d, uint16_t mediaId, iGmMediaInfo *info_out) {
98 return info_Media(d, (iMediaId){ download_MediaType, mediaId }, info_out);
99}
100
101iInt2 imageSize_Media (const iMedia *, iMediaId imageId);
102SDL_Texture * imageTexture_Media (const iMedia *, iMediaId imageId);
103
104size_t numAudio_Media (const iMedia *);
105iPlayer * audioPlayer_Media (const iMedia *, iMediaId audioId);
106void pauseAllPlayers_Media (const iMedia *, iBool setPaused);
65 107
66iMediaId findLinkDownload_Media (const iMedia *, uint16_t linkId);
67iBool downloadInfo_Media (const iMedia *, iMediaId downloadId, iGmMediaInfo *info_out);
68void downloadStats_Media (const iMedia *, iMediaId downloadId, const iString **path_out, 108void downloadStats_Media (const iMedia *, iMediaId downloadId, const iString **path_out,
69 float *bytesPerSecond_out, iBool *isFinished_out); 109 float *bytesPerSecond_out, iBool *isFinished_out);
70 110
111iDeclareType(FontpackMediaInfo)
112
113struct Impl_FontpackMediaInfo {
114 iFontPackId packId;
115 iBool isValid;
116 iBool isInstalled;
117 iBool isReadOnly;
118 size_t sizeInBytes;
119 iStringList *names;
120};
121
122void fontpackInfo_Media (const iMedia *, iMediaId fontpackId,
123 iFontpackMediaInfo *info_out);
124
71/*----------------------------------------------------------------------------------------------*/ 125/*----------------------------------------------------------------------------------------------*/
72 126
73iDeclareType(GmRequest) 127iDeclareType(GmRequest)
@@ -78,7 +132,7 @@ iDeclareClass(MediaRequest)
78struct Impl_MediaRequest { 132struct Impl_MediaRequest {
79 iObject object; 133 iObject object;
80 iDocumentWidget *doc; 134 iDocumentWidget *doc;
81 unsigned int linkId; 135 unsigned int linkId;
82 iGmRequest * req; 136 iGmRequest * req;
83}; 137};
84 138
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) {
400 addAction_Widget(w, navigateRoot_KeyShortcut, "navigate.root"); 400 addAction_Widget(w, navigateRoot_KeyShortcut, "navigate.root");
401} 401}
402 402
403void cancelAllRequests_DocumentWidget(iDocumentWidget *d) {
404 iForEach(ObjectList, i, d->media) {
405 iMediaRequest *mr = i.object;
406 cancel_GmRequest(mr->req);
407 }
408 if (d->request) {
409 cancel_GmRequest(d->request);
410 }
411}
412
403void deinit_DocumentWidget(iDocumentWidget *d) { 413void deinit_DocumentWidget(iDocumentWidget *d) {
414 cancelAllRequests_DocumentWidget(d);
404 pauseAllPlayers_Media(media_GmDocument(d->doc), iTrue); 415 pauseAllPlayers_Media(media_GmDocument(d->doc), iTrue);
405 removeTicker_App(animate_DocumentWidget_, d); 416 removeTicker_App(animate_DocumentWidget_, d);
406 removeTicker_App(prerender_DocumentWidget_, d); 417 removeTicker_App(prerender_DocumentWidget_, d);
@@ -564,7 +575,8 @@ static void addVisible_DocumentWidget_(void *context, const iGmRun *run) {
564 pushBack_PtrArray(&d->visibleWideRuns, run); 575 pushBack_PtrArray(&d->visibleWideRuns, run);
565 } 576 }
566 } 577 }
567 if (run->mediaType == audio_GmRunMediaType || run->mediaType == download_GmRunMediaType) { 578 /* Image runs are static so they're drawn as part of the content. */
579 if (run->mediaType && run->mediaType != image_MediaType) {
568 iAssert(run->mediaId); 580 iAssert(run->mediaId);
569 pushBack_PtrArray(&d->visibleMedia, run); 581 pushBack_PtrArray(&d->visibleMedia, run);
570 } 582 }
@@ -758,14 +770,14 @@ static uint32_t mediaUpdateInterval_DocumentWidget_(const iDocumentWidget *d) {
758 uint32_t interval = invalidInterval_; 770 uint32_t interval = invalidInterval_;
759 iConstForEach(PtrArray, i, &d->visibleMedia) { 771 iConstForEach(PtrArray, i, &d->visibleMedia) {
760 const iGmRun *run = i.ptr; 772 const iGmRun *run = i.ptr;
761 if (run->mediaType == audio_GmRunMediaType) { 773 if (run->mediaType == audio_MediaType) {
762 iPlayer *plr = audioPlayer_Media(media_GmDocument(d->doc), run->mediaId); 774 iPlayer *plr = audioPlayer_Media(media_GmDocument(d->doc), mediaId_GmRun(run));
763 if (flags_Player(plr) & adjustingVolume_PlayerFlag || 775 if (flags_Player(plr) & adjustingVolume_PlayerFlag ||
764 (isStarted_Player(plr) && !isPaused_Player(plr))) { 776 (isStarted_Player(plr) && !isPaused_Player(plr))) {
765 interval = iMin(interval, 1000 / 15); 777 interval = iMin(interval, 1000 / 15);
766 } 778 }
767 } 779 }
768 else if (run->mediaType == download_GmRunMediaType) { 780 else if (run->mediaType == download_MediaType) {
769 interval = iMin(interval, 1000); 781 interval = iMin(interval, 1000);
770 } 782 }
771 } 783 }
@@ -784,8 +796,8 @@ static void updateMedia_DocumentWidget_(iDocumentWidget *d) {
784 refresh_Widget(d); 796 refresh_Widget(d);
785 iConstForEach(PtrArray, i, &d->visibleMedia) { 797 iConstForEach(PtrArray, i, &d->visibleMedia) {
786 const iGmRun *run = i.ptr; 798 const iGmRun *run = i.ptr;
787 if (run->mediaType == audio_GmRunMediaType) { 799 if (run->mediaType == audio_MediaType) {
788 iPlayer *plr = audioPlayer_Media(media_GmDocument(d->doc), run->mediaId); 800 iPlayer *plr = audioPlayer_Media(media_GmDocument(d->doc), mediaId_GmRun(run));
789 if (idleTimeMs_Player(plr) > 3000 && ~flags_Player(plr) & volumeGrabbed_PlayerFlag && 801 if (idleTimeMs_Player(plr) > 3000 && ~flags_Player(plr) & volumeGrabbed_PlayerFlag &&
790 flags_Player(plr) & adjustingVolume_PlayerFlag) { 802 flags_Player(plr) & adjustingVolume_PlayerFlag) {
791 setFlags_Player(plr, adjustingVolume_PlayerFlag, iFalse); 803 setFlags_Player(plr, adjustingVolume_PlayerFlag, iFalse);
@@ -1244,6 +1256,9 @@ static const char *zipPageHeading_(const iRangecc mime) {
1244 if (equalCase_Rangecc(mime, "application/gpub+zip")) { 1256 if (equalCase_Rangecc(mime, "application/gpub+zip")) {
1245 return book_Icon " Gempub"; 1257 return book_Icon " Gempub";
1246 } 1258 }
1259 else if (equalCase_Rangecc(mime, mimeType_FontPack)) {
1260 return "\U0001f520 Fontpack";
1261 }
1247 iRangecc type = iNullRange; 1262 iRangecc type = iNullRange;
1248 nextSplit_Rangecc(mime, "/", &type); /* skip the part before the slash */ 1263 nextSplit_Rangecc(mime, "/", &type); /* skip the part before the slash */
1249 nextSplit_Rangecc(mime, "/", &type); 1264 nextSplit_Rangecc(mime, "/", &type);
@@ -1258,165 +1273,175 @@ static const char *zipPageHeading_(const iRangecc mime) {
1258 1273
1259static void postProcessRequestContent_DocumentWidget_(iDocumentWidget *d, iBool isCached) { 1274static void postProcessRequestContent_DocumentWidget_(iDocumentWidget *d, iBool isCached) {
1260 iWidget *w = as_Widget(d); 1275 iWidget *w = as_Widget(d);
1261 delete_Gempub(d->sourceGempub); 1276 /* Gempub page behavior and footer actions. */ {
1262 d->sourceGempub = NULL; 1277 /* TODO: move this to gempub.c */
1263 if (!cmpCase_String(&d->sourceMime, "application/octet-stream") || 1278 delete_Gempub(d->sourceGempub);
1264 !cmpCase_String(&d->sourceMime, mimeType_Gempub) || 1279 d->sourceGempub = NULL;
1265 endsWithCase_String(d->mod.url, ".gpub")) { 1280 if (!cmpCase_String(&d->sourceMime, "application/octet-stream") ||
1266 iGempub *gempub = new_Gempub(); 1281 !cmpCase_String(&d->sourceMime, mimeType_Gempub) ||
1267 if (open_Gempub(gempub, &d->sourceContent)) { 1282 endsWithCase_String(d->mod.url, ".gpub")) {
1268 setBaseUrl_Gempub(gempub, d->mod.url);
1269 setSource_DocumentWidget(d, collect_String(coverPageSource_Gempub(gempub)));
1270 setCStr_String(&d->sourceMime, mimeType_Gempub);
1271 d->sourceGempub = gempub;
1272 }
1273 else {
1274 delete_Gempub(gempub);
1275 }
1276 }
1277 if (!d->sourceGempub) {
1278 const iString *localPath = collect_String(localFilePathFromUrl_String(d->mod.url));
1279 iBool isInside = iFalse;
1280 if (localPath && !fileExists_FileInfo(localPath)) {
1281 /* This URL may refer to a file inside the archive. */
1282 localPath = findContainerArchive_Path(localPath);
1283 isInside = iTrue;
1284 }
1285 if (localPath && equal_CStr(mediaType_Path(localPath), "application/gpub+zip")) {
1286 iGempub *gempub = new_Gempub(); 1283 iGempub *gempub = new_Gempub();
1287 if (openFile_Gempub(gempub, localPath)) { 1284 if (open_Gempub(gempub, &d->sourceContent)) {
1288 setBaseUrl_Gempub(gempub, collect_String(makeFileUrl_String(localPath))); 1285 setBaseUrl_Gempub(gempub, d->mod.url);
1289 if (!isInside) { 1286 setSource_DocumentWidget(d, collect_String(coverPageSource_Gempub(gempub)));
1290 setSource_DocumentWidget(d, collect_String(coverPageSource_Gempub(gempub))); 1287 setCStr_String(&d->sourceMime, mimeType_Gempub);
1291 setCStr_String(&d->sourceMime, mimeType_Gempub);
1292 }
1293 d->sourceGempub = gempub; 1288 d->sourceGempub = gempub;
1294 } 1289 }
1295 else { 1290 else {
1296 delete_Gempub(gempub); 1291 delete_Gempub(gempub);
1297 } 1292 }
1298 } 1293 }
1299 } 1294 if (!d->sourceGempub) {
1300 if (d->sourceGempub) { 1295 const iString *localPath = collect_String(localFilePathFromUrl_String(d->mod.url));
1301 if (equal_String(d->mod.url, coverPageUrl_Gempub(d->sourceGempub))) { 1296 iBool isInside = iFalse;
1302 if (!isRemote_Gempub(d->sourceGempub)) { 1297 if (localPath && !fileExists_FileInfo(localPath)) {
1303 iArray *items = collectNew_Array(sizeof(iMenuItem)); 1298 /* This URL may refer to a file inside the archive. */
1304 pushBack_Array( 1299 localPath = findContainerArchive_Path(localPath);
1305 items, 1300 isInside = iTrue;
1306 &(iMenuItem){ book_Icon " ${gempub.cover.view}",
1307 0,
1308 0,
1309 format_CStr("!open url:%s",
1310 cstr_String(indexPageUrl_Gempub(d->sourceGempub))) });
1311 if (navSize_Gempub(d->sourceGempub) > 0) {
1312 pushBack_Array(
1313 items,
1314 &(iMenuItem){
1315 format_CStr(forwardArrow_Icon " %s",
1316 cstr_String(navLinkLabel_Gempub(d->sourceGempub, 0))),
1317 SDLK_RIGHT,
1318 0,
1319 format_CStr("!open url:%s",
1320 cstr_String(navLinkUrl_Gempub(d->sourceGempub, 0))) });
1321 }
1322 makeFooterButtons_DocumentWidget_(d, constData_Array(items), size_Array(items));
1323 } 1301 }
1324 else { 1302 if (localPath && equal_CStr(mediaType_Path(localPath), mimeType_Gempub)) {
1325 makeFooterButtons_DocumentWidget_( 1303 iGempub *gempub = new_Gempub();
1326 d, 1304 if (openFile_Gempub(gempub, localPath)) {
1327 (iMenuItem[]){ { book_Icon " ${menu.save.downloads.open}", 1305 setBaseUrl_Gempub(gempub, collect_String(makeFileUrl_String(localPath)));
1328 SDLK_s, 1306 if (!isInside) {
1329 KMOD_PRIMARY | KMOD_SHIFT, 1307 setSource_DocumentWidget(d, collect_String(coverPageSource_Gempub(gempub)));
1330 "document.save open:1" }, 1308 setCStr_String(&d->sourceMime, mimeType_Gempub);
1331 { download_Icon " " saveToDownloads_Label, 1309 }
1332 SDLK_s, 1310 d->sourceGempub = gempub;
1333 KMOD_PRIMARY, 1311 }
1334 "document.save" } }, 1312 else {
1335 2); 1313 delete_Gempub(gempub);
1336 } 1314 }
1337 if (preloadCoverImage_Gempub(d->sourceGempub, d->doc)) {
1338 redoLayout_GmDocument(d->doc);
1339 updateVisible_DocumentWidget_(d);
1340 invalidate_DocumentWidget_(d);
1341 } 1315 }
1342 } 1316 }
1343 else if (equal_String(d->mod.url, indexPageUrl_Gempub(d->sourceGempub))) { 1317 if (d->sourceGempub) {
1344 makeFooterButtons_DocumentWidget_( 1318 if (equal_String(d->mod.url, coverPageUrl_Gempub(d->sourceGempub))) {
1345 d, 1319 if (!isRemote_Gempub(d->sourceGempub)) {
1346 (iMenuItem[]){ { format_CStr(book_Icon " %s", 1320 iArray *items = collectNew_Array(sizeof(iMenuItem));
1347 cstr_String(property_Gempub(d->sourceGempub,
1348 title_GempubProperty))),
1349 SDLK_LEFT,
1350 0,
1351 format_CStr("!open url:%s",
1352 cstr_String(coverPageUrl_Gempub(d->sourceGempub))) } },
1353 1);
1354 }
1355 else {
1356 /* Navigation buttons. */
1357 iArray *items = collectNew_Array(sizeof(iMenuItem));
1358 const size_t navIndex = navIndex_Gempub(d->sourceGempub, d->mod.url);
1359 if (navIndex != iInvalidPos) {
1360 if (navIndex < navSize_Gempub(d->sourceGempub) - 1) {
1361 pushBack_Array( 1321 pushBack_Array(
1362 items, 1322 items,
1363 &(iMenuItem){ 1323 &(iMenuItem){ book_Icon " ${gempub.cover.view}",
1364 format_CStr(forwardArrow_Icon " %s", 1324 0,
1365 cstr_String(navLinkLabel_Gempub(d->sourceGempub, navIndex + 1))), 1325 0,
1366 SDLK_RIGHT, 1326 format_CStr("!open url:%s",
1367 0, 1327 cstr_String(indexPageUrl_Gempub(d->sourceGempub))) });
1368 format_CStr("!open url:%s", 1328 if (navSize_Gempub(d->sourceGempub) > 0) {
1369 cstr_String(navLinkUrl_Gempub(d->sourceGempub, navIndex + 1))) }); 1329 pushBack_Array(
1330 items,
1331 &(iMenuItem){
1332 format_CStr(forwardArrow_Icon " %s",
1333 cstr_String(navLinkLabel_Gempub(d->sourceGempub, 0))),
1334 SDLK_RIGHT,
1335 0,
1336 format_CStr("!open url:%s",
1337 cstr_String(navLinkUrl_Gempub(d->sourceGempub, 0))) });
1338 }
1339 makeFooterButtons_DocumentWidget_(d, constData_Array(items), size_Array(items));
1370 } 1340 }
1371 if (navIndex > 0) { 1341 else {
1372 pushBack_Array( 1342 makeFooterButtons_DocumentWidget_(
1373 items, 1343 d,
1374 &(iMenuItem){ 1344 (iMenuItem[]){ { book_Icon " ${menu.save.downloads.open}",
1375 format_CStr(backArrow_Icon " %s", 1345 SDLK_s,
1376 cstr_String(navLinkLabel_Gempub(d->sourceGempub, navIndex - 1))), 1346 KMOD_PRIMARY | KMOD_SHIFT,
1377 SDLK_LEFT, 1347 "document.save open:1" },
1378 0, 1348 { download_Icon " " saveToDownloads_Label,
1379 format_CStr("!open url:%s", 1349 SDLK_s,
1380 cstr_String(navLinkUrl_Gempub(d->sourceGempub, navIndex - 1))) }); 1350 KMOD_PRIMARY,
1351 "document.save" } },
1352 2);
1381 } 1353 }
1382 else if (!equalCase_String(d->mod.url, indexPageUrl_Gempub(d->sourceGempub))) { 1354 if (preloadCoverImage_Gempub(d->sourceGempub, d->doc)) {
1383 pushBack_Array( 1355 redoLayout_GmDocument(d->doc);
1384 items, 1356 updateVisible_DocumentWidget_(d);
1385 &(iMenuItem){ 1357 invalidate_DocumentWidget_(d);
1386 format_CStr(book_Icon " %s",
1387 cstr_String(property_Gempub(d->sourceGempub, title_GempubProperty))),
1388 SDLK_LEFT,
1389 0,
1390 format_CStr("!open url:%s",
1391 cstr_String(coverPageUrl_Gempub(d->sourceGempub))) });
1392 } 1358 }
1393 } 1359 }
1394 if (!isEmpty_Array(items)) { 1360 else if (equal_String(d->mod.url, indexPageUrl_Gempub(d->sourceGempub))) {
1395 makeFooterButtons_DocumentWidget_(d, constData_Array(items), size_Array(items)); 1361 makeFooterButtons_DocumentWidget_(
1362 d,
1363 (iMenuItem[]){ { format_CStr(book_Icon " %s",
1364 cstr_String(property_Gempub(d->sourceGempub,
1365 title_GempubProperty))),
1366 SDLK_LEFT,
1367 0,
1368 format_CStr("!open url:%s",
1369 cstr_String(coverPageUrl_Gempub(d->sourceGempub))) } },
1370 1);
1396 } 1371 }
1397 } 1372 else {
1398 if (!isCached && prefs_App()->pinSplit && 1373 /* Navigation buttons. */
1399 equal_String(d->mod.url, indexPageUrl_Gempub(d->sourceGempub))) { 1374 iArray *items = collectNew_Array(sizeof(iMenuItem));
1400 const iString *navStart = navStartLinkUrl_Gempub(d->sourceGempub); 1375 const size_t navIndex = navIndex_Gempub(d->sourceGempub, d->mod.url);
1401 if (navStart) { 1376 if (navIndex != iInvalidPos) {
1402 iWindow *win = get_Window(); 1377 if (navIndex < navSize_Gempub(d->sourceGempub) - 1) {
1403 /* Auto-split to show index and the first navigation link. */ 1378 pushBack_Array(
1404 if (numRoots_Window(win) == 2) { 1379 items,
1405 /* This document is showing the index page. */ 1380 &(iMenuItem){
1406 iRoot *other = otherRoot_Window(win, w->root); 1381 format_CStr(forwardArrow_Icon " %s",
1407 postCommandf_Root(other, "open url:%s", cstr_String(navStart)); 1382 cstr_String(navLinkLabel_Gempub(d->sourceGempub, navIndex + 1))),
1408 if (prefs_App()->pinSplit == 1 && w->root == win->roots[1]) { 1383 SDLK_RIGHT,
1409 /* On the wrong side. */ 1384 0,
1410 postCommand_App("ui.split swap:1"); 1385 format_CStr("!open url:%s",
1386 cstr_String(navLinkUrl_Gempub(d->sourceGempub, navIndex + 1))) });
1387 }
1388 if (navIndex > 0) {
1389 pushBack_Array(
1390 items,
1391 &(iMenuItem){
1392 format_CStr(backArrow_Icon " %s",
1393 cstr_String(navLinkLabel_Gempub(d->sourceGempub, navIndex - 1))),
1394 SDLK_LEFT,
1395 0,
1396 format_CStr("!open url:%s",
1397 cstr_String(navLinkUrl_Gempub(d->sourceGempub, navIndex - 1))) });
1398 }
1399 else if (!equalCase_String(d->mod.url, indexPageUrl_Gempub(d->sourceGempub))) {
1400 pushBack_Array(
1401 items,
1402 &(iMenuItem){
1403 format_CStr(book_Icon " %s",
1404 cstr_String(property_Gempub(d->sourceGempub, title_GempubProperty))),
1405 SDLK_LEFT,
1406 0,
1407 format_CStr("!open url:%s",
1408 cstr_String(coverPageUrl_Gempub(d->sourceGempub))) });
1411 } 1409 }
1412 } 1410 }
1413 else { 1411 if (!isEmpty_Array(items)) {
1414 postCommandf_App( 1412 makeFooterButtons_DocumentWidget_(d, constData_Array(items), size_Array(items));
1415 "open newtab:%d url:%s", otherRoot_OpenTabFlag, cstr_String(navStart)); 1413 }
1414 }
1415 if (!isCached && prefs_App()->pinSplit &&
1416 equal_String(d->mod.url, indexPageUrl_Gempub(d->sourceGempub))) {
1417 const iString *navStart = navStartLinkUrl_Gempub(d->sourceGempub);
1418 if (navStart) {
1419 iWindow *win = get_Window();
1420 /* Auto-split to show index and the first navigation link. */
1421 if (numRoots_Window(win) == 2) {
1422 /* This document is showing the index page. */
1423 iRoot *other = otherRoot_Window(win, w->root);
1424 postCommandf_Root(other, "open url:%s", cstr_String(navStart));
1425 if (prefs_App()->pinSplit == 1 && w->root == win->roots[1]) {
1426 /* On the wrong side. */
1427 postCommand_App("ui.split swap:1");
1428 }
1429 }
1430 else {
1431 postCommandf_App(
1432 "open newtab:%d url:%s", otherRoot_OpenTabFlag, cstr_String(navStart));
1433 }
1416 } 1434 }
1417 } 1435 }
1418 } 1436 }
1419 } 1437 }
1438 /* Local fontpacks are automatically shown. */
1439 if (preloadLocalFontpackForPreview_Fonts(d->doc)) {
1440 documentRunsInvalidated_DocumentWidget_(d);
1441 redoLayout_GmDocument(d->doc);
1442 updateVisible_DocumentWidget_(d);
1443 invalidate_DocumentWidget_(d);
1444 }
1420} 1445}
1421 1446
1422static void updateDocument_DocumentWidget_(iDocumentWidget *d, 1447static void updateDocument_DocumentWidget_(iDocumentWidget *d,
@@ -1484,7 +1509,7 @@ static void updateDocument_DocumentWidget_(iDocumentWidget *d,
1484 } 1509 }
1485 delete_String(localPath); 1510 delete_String(localPath);
1486 if (equalCase_Rangecc(urlScheme_String(d->mod.url), "file")) { 1511 if (equalCase_Rangecc(urlScheme_String(d->mod.url), "file")) {
1487 appendFormat_String(&str, "=> %s/ ${doc.archive.view}\n", 1512 appendFormat_String(&str, "=> %s/ " folder_Icon " ${doc.archive.view}\n",
1488 cstr_String(withSpacesEncoded_String(d->mod.url))); 1513 cstr_String(withSpacesEncoded_String(d->mod.url)));
1489 } 1514 }
1490 translate_Lang(&str); 1515 translate_Lang(&str);
@@ -2089,7 +2114,7 @@ static iBool requestMedia_DocumentWidget_(iDocumentWidget *d, iGmLinkId linkId,
2089} 2114}
2090 2115
2091static iBool isDownloadRequest_DocumentWidget(const iDocumentWidget *d, const iMediaRequest *req) { 2116static iBool isDownloadRequest_DocumentWidget(const iDocumentWidget *d, const iMediaRequest *req) {
2092 return findLinkDownload_Media(constMedia_GmDocument(d->doc), req->linkId) != 0; 2117 return findMediaForLink_Media(constMedia_GmDocument(d->doc), req->linkId, download_MediaType).type != 0;
2093} 2118}
2094 2119
2095static iBool handleMediaCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) { 2120static iBool handleMediaCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) {
@@ -2174,7 +2199,7 @@ static void allocVisBuffer_DocumentWidget_(const iDocumentWidget *d) {
2174static iBool fetchNextUnfetchedImage_DocumentWidget_(iDocumentWidget *d) { 2199static iBool fetchNextUnfetchedImage_DocumentWidget_(iDocumentWidget *d) {
2175 iConstForEach(PtrArray, i, &d->visibleLinks) { 2200 iConstForEach(PtrArray, i, &d->visibleLinks) {
2176 const iGmRun *run = i.ptr; 2201 const iGmRun *run = i.ptr;
2177 if (run->linkId && run->mediaType == none_GmRunMediaType && 2202 if (run->linkId && run->mediaType == none_MediaType &&
2178 ~run->flags & decoration_GmRunFlag) { 2203 ~run->flags & decoration_GmRunFlag) {
2179 const int linkFlags = linkFlags_GmDocument(d->doc, run->linkId); 2204 const int linkFlags = linkFlags_GmDocument(d->doc, run->linkId);
2180 if (isMediaLink_GmDocument(d->doc, run->linkId) && 2205 if (isMediaLink_GmDocument(d->doc, run->linkId) &&
@@ -2763,8 +2788,10 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
2763 else if (equalWidget_Command(cmd, w, "document.downloadlink")) { 2788 else if (equalWidget_Command(cmd, w, "document.downloadlink")) {
2764 if (d->contextLink) { 2789 if (d->contextLink) {
2765 const iGmLinkId linkId = d->contextLink->linkId; 2790 const iGmLinkId linkId = d->contextLink->linkId;
2766 setDownloadUrl_Media( 2791 setUrl_Media(media_GmDocument(d->doc),
2767 media_GmDocument(d->doc), linkId, linkUrl_GmDocument(d->doc, linkId)); 2792 linkId,
2793 download_MediaType,
2794 linkUrl_GmDocument(d->doc, linkId));
2768 requestMedia_DocumentWidget_(d, linkId, iFalse /* no filters */); 2795 requestMedia_DocumentWidget_(d, linkId, iFalse /* no filters */);
2769 redoLayout_GmDocument(d->doc); /* inline downloader becomes visible */ 2796 redoLayout_GmDocument(d->doc); /* inline downloader becomes visible */
2770 updateVisible_DocumentWidget_(d); 2797 updateVisible_DocumentWidget_(d);
@@ -2874,7 +2901,7 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
2874 const iMedia * media = media_GmDocument(d->doc); 2901 const iMedia * media = media_GmDocument(d->doc);
2875 const size_t num = numAudio_Media(media); 2902 const size_t num = numAudio_Media(media);
2876 for (size_t id = 1; id <= num; id++) { 2903 for (size_t id = 1; id <= num; id++) {
2877 iPlayer *plr = audioPlayer_Media(media, id); 2904 iPlayer *plr = audioPlayer_Media(media, (iMediaId){ audio_MediaType, id });
2878 if (plr != startedPlr) { 2905 if (plr != startedPlr) {
2879 setPaused_Player(plr, iTrue); 2906 setPaused_Player(plr, iTrue);
2880 } 2907 }
@@ -3212,8 +3239,8 @@ static iRect runRect_DocumentWidget_(const iDocumentWidget *d, const iGmRun *run
3212} 3239}
3213 3240
3214static void setGrabbedPlayer_DocumentWidget_(iDocumentWidget *d, const iGmRun *run) { 3241static void setGrabbedPlayer_DocumentWidget_(iDocumentWidget *d, const iGmRun *run) {
3215 if (run && run->mediaType == audio_GmRunMediaType) { 3242 if (run && run->mediaType == audio_MediaType) {
3216 iPlayer *plr = audioPlayer_Media(media_GmDocument(d->doc), run->mediaId); 3243 iPlayer *plr = audioPlayer_Media(media_GmDocument(d->doc), mediaId_GmRun(run));
3217 setFlags_Player(plr, volumeGrabbed_PlayerFlag, iTrue); 3244 setFlags_Player(plr, volumeGrabbed_PlayerFlag, iTrue);
3218 d->grabbedStartVolume = volume_Player(plr); 3245 d->grabbedStartVolume = volume_Player(plr);
3219 d->grabbedPlayer = run; 3246 d->grabbedPlayer = run;
@@ -3221,7 +3248,7 @@ static void setGrabbedPlayer_DocumentWidget_(iDocumentWidget *d, const iGmRun *r
3221 } 3248 }
3222 else if (d->grabbedPlayer) { 3249 else if (d->grabbedPlayer) {
3223 setFlags_Player( 3250 setFlags_Player(
3224 audioPlayer_Media(media_GmDocument(d->doc), d->grabbedPlayer->mediaId), 3251 audioPlayer_Media(media_GmDocument(d->doc), mediaId_GmRun(d->grabbedPlayer)),
3225 volumeGrabbed_PlayerFlag, 3252 volumeGrabbed_PlayerFlag,
3226 iFalse); 3253 iFalse);
3227 d->grabbedPlayer = NULL; 3254 d->grabbedPlayer = NULL;
@@ -3249,11 +3276,21 @@ static iBool processMediaEvents_DocumentWidget_(iDocumentWidget *d, const SDL_Ev
3249 const iInt2 mouse = init_I2(ev->button.x, ev->button.y); 3276 const iInt2 mouse = init_I2(ev->button.x, ev->button.y);
3250 iConstForEach(PtrArray, i, &d->visibleMedia) { 3277 iConstForEach(PtrArray, i, &d->visibleMedia) {
3251 const iGmRun *run = i.ptr; 3278 const iGmRun *run = i.ptr;
3252 if (run->mediaType != audio_GmRunMediaType) { 3279 if (run->mediaType == fontpack_MediaType) {
3280 iFontpackUI ui;
3281 init_FontpackUI(&ui, media_GmDocument(d->doc), run->mediaId,
3282 runRect_DocumentWidget_(d, run));
3283 if (processEvent_FontpackUI(&ui, ev)) {
3284 refresh_Widget(d);
3285 return iTrue;
3286 }
3287 }
3288 if (run->mediaType != audio_MediaType) {
3253 continue; 3289 continue;
3254 } 3290 }
3291 /* TODO: move this to mediaui.c */
3255 const iRect rect = runRect_DocumentWidget_(d, run); 3292 const iRect rect = runRect_DocumentWidget_(d, run);
3256 iPlayer * plr = audioPlayer_Media(media_GmDocument(d->doc), run->mediaId); 3293 iPlayer * plr = audioPlayer_Media(media_GmDocument(d->doc), mediaId_GmRun(run));
3257 if (contains_Rect(rect, mouse)) { 3294 if (contains_Rect(rect, mouse)) {
3258 iPlayerUI ui; 3295 iPlayerUI ui;
3259 init_PlayerUI(&ui, plr, rect); 3296 init_PlayerUI(&ui, plr, rect);
@@ -3633,7 +3670,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
3633 cstr_String(linkUrl)) }, 3670 cstr_String(linkUrl)) },
3634 }, 3671 },
3635 3); 3672 3);
3636 if (isNative && d->contextLink->mediaType != download_GmRunMediaType) { 3673 if (isNative && d->contextLink->mediaType != download_MediaType) {
3637 pushBackN_Array(&items, (iMenuItem[]){ 3674 pushBackN_Array(&items, (iMenuItem[]){
3638 { "---" }, 3675 { "---" },
3639 { download_Icon " ${link.download}", 0, 0, "document.downloadlink" }, 3676 { download_Icon " ${link.download}", 0, 0, "document.downloadlink" },
@@ -3641,7 +3678,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
3641 } 3678 }
3642 iMediaRequest *mediaReq; 3679 iMediaRequest *mediaReq;
3643 if ((mediaReq = findMediaRequest_DocumentWidget_(d, d->contextLink->linkId)) != NULL && 3680 if ((mediaReq = findMediaRequest_DocumentWidget_(d, d->contextLink->linkId)) != NULL &&
3644 d->contextLink->mediaType != download_GmRunMediaType) { 3681 d->contextLink->mediaType != download_MediaType) {
3645 if (isFinished_GmRequest(mediaReq->req)) { 3682 if (isFinished_GmRequest(mediaReq->req)) {
3646 pushBack_Array(&items, 3683 pushBack_Array(&items,
3647 &(iMenuItem){ download_Icon " " saveToDownloads_Label, 3684 &(iMenuItem){ download_Icon " " saveToDownloads_Label,
@@ -3763,7 +3800,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
3763 case drag_ClickResult: { 3800 case drag_ClickResult: {
3764 if (d->grabbedPlayer) { 3801 if (d->grabbedPlayer) {
3765 iPlayer *plr = 3802 iPlayer *plr =
3766 audioPlayer_Media(media_GmDocument(d->doc), d->grabbedPlayer->mediaId); 3803 audioPlayer_Media(media_GmDocument(d->doc), mediaId_GmRun(d->grabbedPlayer));
3767 iPlayerUI ui; 3804 iPlayerUI ui;
3768 init_PlayerUI(&ui, plr, runRect_DocumentWidget_(d, d->grabbedPlayer)); 3805 init_PlayerUI(&ui, plr, runRect_DocumentWidget_(d, d->grabbedPlayer));
3769 float off = (float) delta_Click(&d->click).x / (float) width_Rect(ui.volumeSlider); 3806 float off = (float) delta_Click(&d->click).x / (float) width_Rect(ui.volumeSlider);
@@ -3883,8 +3920,9 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
3883 } 3920 }
3884 if (d->hoverLink) { 3921 if (d->hoverLink) {
3885 /* TODO: Move this to a method. */ 3922 /* TODO: Move this to a method. */
3886 const iGmLinkId linkId = d->hoverLink->linkId; 3923 const iGmLinkId linkId = d->hoverLink->linkId;
3887 const int linkFlags = linkFlags_GmDocument(d->doc, linkId); 3924 const iMediaId linkMedia = mediaId_GmRun(d->hoverLink);
3925 const int linkFlags = linkFlags_GmDocument(d->doc, linkId);
3888 iAssert(linkId); 3926 iAssert(linkId);
3889 /* Media links are opened inline by default. */ 3927 /* Media links are opened inline by default. */
3890 if (isMediaLink_GmDocument(d->doc, linkId)) { 3928 if (isMediaLink_GmDocument(d->doc, linkId)) {
@@ -3937,6 +3975,12 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
3937 } 3975 }
3938 refresh_Widget(w); 3976 refresh_Widget(w);
3939 } 3977 }
3978 else if (linkMedia.type == download_MediaType ||
3979 findMediaRequest_DocumentWidget_(d, linkId)) {
3980 /* TODO: What should be done when clicking on an inline download?
3981 Maybe dismiss if finished? */
3982 return iTrue;
3983 }
3940 else if (linkFlags & supportedScheme_GmLinkFlag) { 3984 else if (linkFlags & supportedScheme_GmLinkFlag) {
3941 int tabMode = openTabMode_Sym(modState_Keys()); 3985 int tabMode = openTabMode_Sym(modState_Keys());
3942 if (isPinned_DocumentWidget_(d)) { 3986 if (isPinned_DocumentWidget_(d)) {
@@ -4071,7 +4115,7 @@ static void fillRange_DrawContext_(iDrawContext *d, const iGmRun *run, enum iCol
4071 4115
4072static void drawMark_DrawContext_(void *context, const iGmRun *run) { 4116static void drawMark_DrawContext_(void *context, const iGmRun *run) {
4073 iDrawContext *d = context; 4117 iDrawContext *d = context;
4074 if (run->mediaType == none_GmRunMediaType) { 4118 if (run->mediaType == none_MediaType) {
4075 fillRange_DrawContext_(d, run, uiMatching_ColorId, d->widget->foundMark, &d->inFoundMark); 4119 fillRange_DrawContext_(d, run, uiMatching_ColorId, d->widget->foundMark, &d->inFoundMark);
4076 fillRange_DrawContext_(d, run, uiMarked_ColorId, d->widget->selectMark, &d->inSelectMark); 4120 fillRange_DrawContext_(d, run, uiMarked_ColorId, d->widget->selectMark, &d->inSelectMark);
4077 } 4121 }
@@ -4178,8 +4222,8 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) {
4178 d->runsDrawn.end = run; 4222 d->runsDrawn.end = run;
4179 } 4223 }
4180 } 4224 }
4181 if (run->mediaType == image_GmRunMediaType) { 4225 if (run->mediaType == image_MediaType) {
4182 SDL_Texture *tex = imageTexture_Media(media_GmDocument(d->widget->doc), run->mediaId); 4226 SDL_Texture *tex = imageTexture_Media(media_GmDocument(d->widget->doc), mediaId_GmRun(run));
4183 const iRect dst = moved_Rect(run->visBounds, origin); 4227 const iRect dst = moved_Rect(run->visBounds, origin);
4184 if (tex) { 4228 if (tex) {
4185 fillRect_Paint(&d->paint, dst, tmBackground_ColorId); /* in case the image has alpha */ 4229 fillRect_Paint(&d->paint, dst, tmBackground_ColorId); /* in case the image has alpha */
@@ -4334,31 +4378,31 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) {
4334 fg = linkColor_GmDocument(doc, run->linkId, textHover_GmLinkPart); 4378 fg = linkColor_GmDocument(doc, run->linkId, textHover_GmLinkPart);
4335 iString text; 4379 iString text;
4336 init_String(&text); 4380 init_String(&text);
4337 iMediaId imageId = linkImage_GmDocument(doc, run->linkId); 4381 const iMediaId linkMedia = findMediaForLink_Media(constMedia_GmDocument(doc),
4338 iMediaId audioId = !imageId ? linkAudio_GmDocument(doc, run->linkId) : 0; 4382 run->linkId, none_MediaType);
4339 iMediaId downloadId = !imageId && !audioId ? 4383 iAssert(linkMedia.type != none_MediaType);
4340 findLinkDownload_Media(constMedia_GmDocument(doc), run->linkId) : 0; 4384 iGmMediaInfo info;
4341 iAssert(imageId || audioId || downloadId); 4385 info_Media(constMedia_GmDocument(doc), linkMedia, &info);
4342 if (imageId) { 4386 switch (linkMedia.type) {
4343 iAssert(!isEmpty_Rect(run->bounds)); 4387 case image_MediaType: {
4344 iGmMediaInfo info; 4388 iAssert(!isEmpty_Rect(run->bounds));
4345 imageInfo_Media(constMedia_GmDocument(doc), imageId, &info); 4389 const iInt2 imgSize = imageSize_Media(constMedia_GmDocument(doc), linkMedia);
4346 const iInt2 imgSize = imageSize_Media(constMedia_GmDocument(doc), imageId); 4390 format_String(&text, "%s \u2014 %d x %d \u2014 %.1f%s",
4347 format_String(&text, "%s \u2014 %d x %d \u2014 %.1f%s", 4391 info.type, imgSize.x, imgSize.y, info.numBytes / 1.0e6f,
4348 info.type, imgSize.x, imgSize.y, info.numBytes / 1.0e6f, 4392 cstr_Lang("mb"));
4349 cstr_Lang("mb")); 4393 break;
4350 } 4394 }
4351 else if (audioId) { 4395 case audio_MediaType:
4352 iGmMediaInfo info; 4396 format_String(&text, "%s", info.type);
4353 audioInfo_Media(constMedia_GmDocument(doc), audioId, &info); 4397 break;
4354 format_String(&text, "%s", info.type); 4398 case download_MediaType:
4355 } 4399 format_String(&text, "%s", info.type);
4356 else if (downloadId) { 4400 break;
4357 iGmMediaInfo info; 4401 default:
4358 downloadInfo_Media(constMedia_GmDocument(doc), downloadId, &info); 4402 break;
4359 format_String(&text, "%s", info.type);
4360 } 4403 }
4361 if (findMediaRequest_DocumentWidget_(d->widget, run->linkId)) { 4404 if (linkMedia.type != download_MediaType && /* can't cancel downloads currently */
4405 findMediaRequest_DocumentWidget_(d->widget, run->linkId)) {
4362 appendFormat_String( 4406 appendFormat_String(
4363 &text, " %s" close_Icon, isHover ? escape_Color(tmLinkText_ColorId) : ""); 4407 &text, " %s" close_Icon, isHover ? escape_Color(tmLinkText_ColorId) : "");
4364 } 4408 }
@@ -4589,18 +4633,25 @@ static void drawSideElements_DocumentWidget_(const iDocumentWidget *d) {
4589static void drawMedia_DocumentWidget_(const iDocumentWidget *d, iPaint *p) { 4633static void drawMedia_DocumentWidget_(const iDocumentWidget *d, iPaint *p) {
4590 iConstForEach(PtrArray, i, &d->visibleMedia) { 4634 iConstForEach(PtrArray, i, &d->visibleMedia) {
4591 const iGmRun * run = i.ptr; 4635 const iGmRun * run = i.ptr;
4592 if (run->mediaType == audio_GmRunMediaType) { 4636 if (run->mediaType == audio_MediaType) {
4593 iPlayerUI ui; 4637 iPlayerUI ui;
4594 init_PlayerUI(&ui, 4638 init_PlayerUI(&ui,
4595 audioPlayer_Media(media_GmDocument(d->doc), run->mediaId), 4639 audioPlayer_Media(media_GmDocument(d->doc), mediaId_GmRun(run)),
4596 runRect_DocumentWidget_(d, run)); 4640 runRect_DocumentWidget_(d, run));
4597 draw_PlayerUI(&ui, p); 4641 draw_PlayerUI(&ui, p);
4598 } 4642 }
4599 else if (run->mediaType == download_GmRunMediaType) { 4643 else if (run->mediaType == download_MediaType) {
4600 iDownloadUI ui; 4644 iDownloadUI ui;
4601 init_DownloadUI(&ui, d, run->mediaId, runRect_DocumentWidget_(d, run)); 4645 init_DownloadUI(&ui, constMedia_GmDocument(d->doc), run->mediaId,
4646 runRect_DocumentWidget_(d, run));
4602 draw_DownloadUI(&ui, p); 4647 draw_DownloadUI(&ui, p);
4603 } 4648 }
4649 else if (run->mediaType == fontpack_MediaType) {
4650 iFontpackUI ui;
4651 init_FontpackUI(&ui, constMedia_GmDocument(d->doc), run->mediaId,
4652 runRect_DocumentWidget_(d, run));
4653 draw_FontpackUI(&ui, p);
4654 }
4604 } 4655 }
4605} 4656}
4606 4657
diff --git a/src/ui/documentwidget.h b/src/ui/documentwidget.h
index cc09c72d..1f2ecfc0 100644
--- a/src/ui/documentwidget.h
+++ b/src/ui/documentwidget.h
@@ -32,6 +32,8 @@ iDeclareType(History)
32iDeclareWidgetClass(DocumentWidget) 32iDeclareWidgetClass(DocumentWidget)
33iDeclareObjectConstruction(DocumentWidget) 33iDeclareObjectConstruction(DocumentWidget)
34 34
35void cancelAllRequests_DocumentWidget(iDocumentWidget *);
36
35void serializeState_DocumentWidget (const iDocumentWidget *, iStream *outs); 37void serializeState_DocumentWidget (const iDocumentWidget *, iStream *outs);
36void deserializeState_DocumentWidget (iDocumentWidget *, iStream *ins); 38void deserializeState_DocumentWidget (iDocumentWidget *, iStream *ins);
37 39
diff --git a/src/ui/mediaui.c b/src/ui/mediaui.c
index 22552027..aa45d73a 100644
--- a/src/ui/mediaui.c
+++ b/src/ui/mediaui.c
@@ -30,6 +30,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
30#include "lang.h" 30#include "lang.h"
31 31
32#include <the_Foundation/path.h> 32#include <the_Foundation/path.h>
33#include <the_Foundation/stringlist.h>
33 34
34static const char *volumeChar_(float volume) { 35static const char *volumeChar_(float volume) {
35 if (volume <= 0) { 36 if (volume <= 0) {
@@ -61,7 +62,7 @@ void init_PlayerUI(iPlayerUI *d, const iPlayer *player, iRect bounds) {
61 } 62 }
62} 63}
63 64
64static void drawPlayerButton_(iPaint *p, iRect rect, const char *label, int font) { 65static void drawInlineButton_(iPaint *p, iRect rect, const char *label, int font) {
65 const iInt2 mouse = mouseCoord_Window(get_Window(), 0); 66 const iInt2 mouse = mouseCoord_Window(get_Window(), 0);
66 const iBool isHover = contains_Rect(rect, mouse); 67 const iBool isHover = contains_Rect(rect, mouse);
67 const iBool isPressed = isHover && (SDL_GetMouseState(NULL, NULL) & SDL_BUTTON_LEFT) != 0; 68 const iBool isPressed = isHover && (SDL_GetMouseState(NULL, NULL) & SDL_BUTTON_LEFT) != 0;
@@ -113,14 +114,14 @@ void draw_PlayerUI(iPlayerUI *d, iPaint *p) {
113 const iBool isAdjusting = (flags_Player(d->player) & adjustingVolume_PlayerFlag) != 0; 114 const iBool isAdjusting = (flags_Player(d->player) & adjustingVolume_PlayerFlag) != 0;
114 fillRect_Paint(p, d->bounds, playerBackground_ColorId); 115 fillRect_Paint(p, d->bounds, playerBackground_ColorId);
115 drawRect_Paint(p, d->bounds, playerFrame_ColorId); 116 drawRect_Paint(p, d->bounds, playerFrame_ColorId);
116 drawPlayerButton_(p, 117 drawInlineButton_(p,
117 d->playPauseRect, 118 d->playPauseRect,
118 isPaused_Player(d->player) ? "\U0001f782" : "\u23f8", 119 isPaused_Player(d->player) ? "\U0001f782" : "\u23f8",
119 uiContent_FontId); 120 uiContent_FontId);
120 drawPlayerButton_(p, d->rewindRect, "\u23ee", uiContent_FontId); 121 drawInlineButton_(p, d->rewindRect, "\u23ee", uiContent_FontId);
121 drawPlayerButton_(p, d->menuRect, menu_Icon, uiContent_FontId); 122 drawInlineButton_(p, d->menuRect, menu_Icon, uiContent_FontId);
122 if (!isAdjusting) { 123 if (!isAdjusting) {
123 drawPlayerButton_( 124 drawInlineButton_(
124 p, d->volumeRect, volumeChar_(volume_Player(d->player)), uiContentSymbols_FontId); 125 p, d->volumeRect, volumeChar_(volume_Player(d->player)), uiContentSymbols_FontId);
125 } 126 }
126 const int hgt = lineHeight_Text(uiLabelBig_FontId); 127 const int hgt = lineHeight_Text(uiLabelBig_FontId);
@@ -228,24 +229,26 @@ static void drawSevenSegmentBytes_(iInt2 pos, int color, size_t numBytes) {
228 deinit_String(&digits); 229 deinit_String(&digits);
229} 230}
230 231
231void init_DownloadUI(iDownloadUI *d, const iDocumentWidget *doc, uint16_t mediaId, iRect bounds) { 232void init_DownloadUI(iDownloadUI *d, const iMedia *media, uint16_t mediaId, iRect bounds) {
232 d->doc = doc; 233 d->media = media;
233 d->mediaId = mediaId; 234 d->mediaId = mediaId;
234 d->bounds = bounds; 235 d->bounds = bounds;
235} 236}
236 237
238/*----------------------------------------------------------------------------------------------*/
239
237iBool processEvent_DownloadUI(iDownloadUI *d, const SDL_Event *ev) { 240iBool processEvent_DownloadUI(iDownloadUI *d, const SDL_Event *ev) {
238 return iFalse; 241 return iFalse;
239} 242}
240 243
241void draw_DownloadUI(const iDownloadUI *d, iPaint *p) { 244void draw_DownloadUI(const iDownloadUI *d, iPaint *p) {
242 const iMedia *media = constMedia_GmDocument(document_DocumentWidget(d->doc));
243 iGmMediaInfo info; 245 iGmMediaInfo info;
244 float bytesPerSecond; 246 float bytesPerSecond;
245 const iString *path; 247 const iString *path;
246 iBool isFinished; 248 iBool isFinished;
247 downloadInfo_Media(media, d->mediaId, &info); 249 downloadInfo_Media(d->media, d->mediaId, &info);
248 downloadStats_Media(media, d->mediaId, &path, &bytesPerSecond, &isFinished); 250 downloadStats_Media(d->media, (iMediaId){ download_MediaType, d->mediaId },
251 &path, &bytesPerSecond, &isFinished);
249 fillRect_Paint(p, d->bounds, uiBackground_ColorId); 252 fillRect_Paint(p, d->bounds, uiBackground_ColorId);
250 drawRect_Paint(p, d->bounds, uiSeparator_ColorId); 253 drawRect_Paint(p, d->bounds, uiSeparator_ColorId);
251 iRect rect = d->bounds; 254 iRect rect = d->bounds;
@@ -275,3 +278,68 @@ void draw_DownloadUI(const iDownloadUI *d, iPaint *p) {
275 translateCStr_Lang("\u2014 ${mb.per.sec}")); 278 translateCStr_Lang("\u2014 ${mb.per.sec}"));
276 } 279 }
277} 280}
281
282/*----------------------------------------------------------------------------------------------*/
283
284void init_FontpackUI(iFontpackUI *d, const iMedia *media, uint16_t mediaId, iRect bounds) {
285 d->media = media;
286 d->mediaId = mediaId;
287 d->bounds = bounds;
288 d->installRect.size = add_I2(measure_Text(uiLabel_FontId, "${media.fontpack.install}").bounds.size,
289 muli_I2(gap2_UI, 3));
290 d->installRect.pos.x = right_Rect(d->bounds) - gap_UI - d->installRect.size.x;
291 d->installRect.pos.y = mid_Rect(d->bounds).y - d->installRect.size.y / 2;
292}
293
294iBool processEvent_FontpackUI(iFontpackUI *d, const SDL_Event *ev) {
295 switch (ev->type) {
296 case SDL_MOUSEBUTTONDOWN:
297 case SDL_MOUSEBUTTONUP: {
298 const iInt2 pos = init_I2(ev->button.x, ev->button.y);
299 if (contains_Rect(d->installRect, pos)) {
300 return iTrue;
301 }
302 break;
303 }
304 case SDL_MOUSEMOTION:
305 if (contains_Rect(d->bounds, init_I2(ev->motion.x, ev->motion.y))) {
306 return iTrue;
307 }
308 break;
309 }
310 return iFalse;
311}
312
313int height_FontpackUI(const iMedia *media, uint16_t mediaId, int width) {
314 const iStringList *names;
315 iFontpackMediaInfo info;
316 fontpackInfo_Media(media, (iMediaId){ fontpack_MediaType, mediaId }, &info);
317 return lineHeight_Text(uiContent_FontId) +
318 lineHeight_Text(uiLabel_FontId) * (1 + size_StringList(info.names));
319}
320
321void draw_FontpackUI(const iFontpackUI *d, iPaint *p) {
322 /* Draw a background box. */
323 fillRect_Paint(p, d->bounds, uiBackground_ColorId);
324 drawRect_Paint(p, d->bounds, uiSeparator_ColorId);
325 iFontpackMediaInfo info;
326 fontpackInfo_Media(d->media, (iMediaId){ fontpack_MediaType, d->mediaId }, &info);
327 iInt2 pos = topLeft_Rect(d->bounds);
328 const char *checks[] = { "\u2610", "\u2611" };
329 draw_Text(uiContentBold_FontId, pos, uiHeading_ColorId, "\"%s\" v%d",
330 cstr_String(info.packId.id), info.packId.version);
331 pos.y += lineHeight_Text(uiContentBold_FontId);
332 draw_Text(uiLabelBold_FontId, pos, uiText_ColorId, "%.1f MB, %d fonts %s %s %s",
333 info.sizeInBytes / 1.0e6, size_StringList(info.names),
334// checks[info.isValid], info.isValid ? "No errors" : "Errors detected",
335 checks[info.isInstalled], info.isInstalled ? "Installed" : "Not installed",
336 info.isReadOnly ? "Read-Only" : "");
337 pos.y += lineHeight_Text(uiLabelBold_FontId);
338 iConstForEach(StringList, i, info.names) {
339 drawRange_Text(uiLabel_FontId, pos, uiText_ColorId, range_String(i.value));
340 pos.y += lineHeight_Text(uiLabel_FontId);
341 }
342 /* Buttons. */
343 drawInlineButton_(p, d->installRect,
344 "${media.fontpack.install}", uiLabel_FontId);
345}
diff --git a/src/ui/mediaui.h b/src/ui/mediaui.h
index e79dedc0..73de1994 100644
--- a/src/ui/mediaui.h
+++ b/src/ui/mediaui.h
@@ -51,11 +51,27 @@ iDeclareType(Media)
51iDeclareType(DownloadUI) 51iDeclareType(DownloadUI)
52 52
53struct Impl_DownloadUI { 53struct Impl_DownloadUI {
54 const iDocumentWidget *doc; 54 const iMedia *media;
55 uint16_t mediaId; 55 uint16_t mediaId;
56 iRect bounds; 56 iRect bounds;
57}; 57};
58 58
59void init_DownloadUI (iDownloadUI *, const iDocumentWidget *doc, uint16_t mediaId, iRect bounds); 59void init_DownloadUI (iDownloadUI *, const iMedia *media, uint16_t mediaId, iRect bounds);
60iBool processEvent_DownloadUI (iDownloadUI *, const SDL_Event *ev); 60iBool processEvent_DownloadUI (iDownloadUI *, const SDL_Event *ev);
61void draw_DownloadUI (const iDownloadUI *, iPaint *p); 61void draw_DownloadUI (const iDownloadUI *, iPaint *p);
62
63/*----------------------------------------------------------------------------------------------*/
64
65iDeclareType(FontpackUI)
66
67struct Impl_FontpackUI {
68 const iMedia *media;
69 uint16_t mediaId;
70 iRect bounds;
71 iRect installRect;
72};
73
74void init_FontpackUI (iFontpackUI *, const iMedia *media, uint16_t mediaId, iRect bounds);
75int height_FontpackUI (const iMedia *media, uint16_t mediaId, int width);
76iBool processEvent_FontpackUI (iFontpackUI *, const SDL_Event *ev);
77void draw_FontpackUI (const iFontpackUI *, iPaint *p);