diff --git a/README.md b/README.md
index a09d98a..1b55a44 100644
--- a/README.md
+++ b/README.md
@@ -19,51 +19,16 @@ TODO:
10. < input type=**datetime** />, essentially a type "date" but with a vertical separator, then `mm | ss | FORMAT` layout for time.
11. **< table >** component. [🔗 Related Godot proposal](https://github.com/godotengine/godot-proposals/issues/97)
12. **< canvas >** component should be theoretically impossible by exposing Godot `_draw()` APIs to Lua.
+13. `grid` display property for CSS, using `GridContainer` in Godot.
Issues:
1. **< br />** counts as 1 element in **WebsiteContainer**, therefore despite being (0,0) in size, it counts as double in spacing
2. **Tween** API doesn't modify CSS, it operates independently at Godot level.
3. Certain properties like `scale` and `rotate` don't apply to the `active` pseudo-class because they rely on mouse_enter and mouse_exit events
4. `
Box
` something like this has the "Box" text (presumably the PanelContainer) as the target of the hover, not the div itself (which has the w/h size)
+5. font in button doesn't comply with CSS, its the projects default
Notes:
- **< input />** is sort-of inline in normal web. We render it as a block element (new-line).
- A single `RichTextLabel` for inline text tags should stop, we should use invididual ones so it's easier to style and achieve separation through a `vboxcontainer`.
- Fonts use **Flash of Unstyled Text (FOUT)** as opposed to **Flash of Invisible Text (FOIT)**, meaning the text with custom fonts will render with a generic font (sans-serif) while the custom ones downloads.
-
-Supported styles:
-
-- **Font style:**
- - `font-bold`
- - `font-italic`
- - `underline`
-- **Font size:**
- - `text-xs` → 12
- - `text-sm` → 14
- - `text-base` → 16
- - `text-lg` → 18
- - `text-xl` → 20
- - `text-2xl` → 24
- - `text-3xl` → 30
- - `text-4xl` → 36
- - `text-5xl` → 48
- - `text-6xl` → 60
-- **Font family:**
- - `font-mono`
-- **Text color:**
- - `text-[color]`
-- **Background color:**
- - `bg-[color]`
-- **Flexbox**
-- `flex` / `inline-flex` (display: flex/inline-flex)
-- `flex-row`, `flex-row-reverse`, `flex-col`, `flex-col-reverse` (flex-direction)
-- `flex-nowrap`, `flex-wrap`, `flex-wrap-reverse` (flex-wrap)
-- `justify-start`, `justify-end`, `justify-center`, `justify-between`, `justify-around`, `justify-evenly` (justify-content)
-- `items-start`, `items-end`, `items-center`, `items-baseline`, `items-stretch` (align-items)
-- `content-start`, `content-end`, `content-center`, `content-between`, `content-around`, `content-evenly`, `content-stretch` (align-content)
-- `gap-{size}`, `row-gap-{size}`, `col-gap-{size}` (gap, row-gap, column-gap)
-- `flex-grow-{n}` (flex-grow)
-- `flex-shrink-{n}` (flex-shrink)
-- `basis-{size}` (flex-basis)
-- `self-auto`, `self-start`, `self-end`, `self-center`, `self-stretch`, `self-baseline` (align-self)
-- `order-{n}` (order)
diff --git a/flumi/Assets/Icons/checkbox_white.svg b/flumi/Assets/Icons/checkbox_white.svg
new file mode 100644
index 0000000..7abd527
--- /dev/null
+++ b/flumi/Assets/Icons/checkbox_white.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/flumi/Assets/Icons/checkbox_white.svg.import b/flumi/Assets/Icons/checkbox_white.svg.import
new file mode 100644
index 0000000..8069288
--- /dev/null
+++ b/flumi/Assets/Icons/checkbox_white.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://blskvfjswul1d"
+path="res://.godot/imported/checkbox_white.svg-3c05c756132e06b6df4f4ae643f84518.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://Assets/Icons/checkbox_white.svg"
+dest_files=["res://.godot/imported/checkbox_white.svg-3c05c756132e06b6df4f4ae643f84518.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/flumi/Assets/Icons/download.svg b/flumi/Assets/Icons/download.svg
new file mode 100644
index 0000000..b228cb9
--- /dev/null
+++ b/flumi/Assets/Icons/download.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/flumi/Assets/Icons/download.svg.import b/flumi/Assets/Icons/download.svg.import
new file mode 100644
index 0000000..55417e9
--- /dev/null
+++ b/flumi/Assets/Icons/download.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cbwitcygwoqdo"
+path="res://.godot/imported/download.svg-a21cd5d191a2f0c8e42168f24a4d40ed.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://Assets/Icons/download.svg"
+dest_files=["res://.godot/imported/download.svg-a21cd5d191a2f0c8e42168f24a4d40ed.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/flumi/Assets/Icons/ellipsis-vertical.svg b/flumi/Assets/Icons/ellipsis-vertical.svg
new file mode 100644
index 0000000..4285526
--- /dev/null
+++ b/flumi/Assets/Icons/ellipsis-vertical.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/flumi/Assets/Icons/ellipsis-vertical.svg.import b/flumi/Assets/Icons/ellipsis-vertical.svg.import
new file mode 100644
index 0000000..6dfe332
--- /dev/null
+++ b/flumi/Assets/Icons/ellipsis-vertical.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cklatjc4m38dy"
+path="res://.godot/imported/ellipsis-vertical.svg-294910634c008df812f7ef47e8a8a214.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://Assets/Icons/ellipsis-vertical.svg"
+dest_files=["res://.godot/imported/ellipsis-vertical.svg-294910634c008df812f7ef47e8a8a214.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/flumi/Assets/Icons/external-link.svg b/flumi/Assets/Icons/external-link.svg
new file mode 100644
index 0000000..3011ad2
--- /dev/null
+++ b/flumi/Assets/Icons/external-link.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/flumi/Assets/Icons/external-link.svg.import b/flumi/Assets/Icons/external-link.svg.import
new file mode 100644
index 0000000..dbfd633
--- /dev/null
+++ b/flumi/Assets/Icons/external-link.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://du24f6em2nqwq"
+path="res://.godot/imported/external-link.svg-075235ddf5518da7b64a7c5332224a40.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://Assets/Icons/external-link.svg"
+dest_files=["res://.godot/imported/external-link.svg-075235ddf5518da7b64a7c5332224a40.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/flumi/Assets/Icons/hat-glasses.svg b/flumi/Assets/Icons/hat-glasses.svg
new file mode 100644
index 0000000..2941f1a
--- /dev/null
+++ b/flumi/Assets/Icons/hat-glasses.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/flumi/Assets/Icons/hat-glasses.svg.import b/flumi/Assets/Icons/hat-glasses.svg.import
new file mode 100644
index 0000000..f5961df
--- /dev/null
+++ b/flumi/Assets/Icons/hat-glasses.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dwxquqmmd6dqx"
+path="res://.godot/imported/hat-glasses.svg-a7dbfb316472ae1702496a78a0ce47e7.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://Assets/Icons/hat-glasses.svg"
+dest_files=["res://.godot/imported/hat-glasses.svg-a7dbfb316472ae1702496a78a0ce47e7.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/flumi/Assets/Icons/history.svg b/flumi/Assets/Icons/history.svg
new file mode 100644
index 0000000..9482942
--- /dev/null
+++ b/flumi/Assets/Icons/history.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/flumi/Assets/Icons/history.svg.import b/flumi/Assets/Icons/history.svg.import
new file mode 100644
index 0000000..16b4cea
--- /dev/null
+++ b/flumi/Assets/Icons/history.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bcaoarwrwqbby"
+path="res://.godot/imported/history.svg-ce846a8aaa80f3d2f2a595f430c7ab7c.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://Assets/Icons/history.svg"
+dest_files=["res://.godot/imported/history.svg-ce846a8aaa80f3d2f2a595f430c7ab7c.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/flumi/Assets/Icons/log-out.svg b/flumi/Assets/Icons/log-out.svg
new file mode 100644
index 0000000..67644b8
--- /dev/null
+++ b/flumi/Assets/Icons/log-out.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/flumi/Assets/Icons/log-out.svg.import b/flumi/Assets/Icons/log-out.svg.import
new file mode 100644
index 0000000..a7df223
--- /dev/null
+++ b/flumi/Assets/Icons/log-out.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cilwaxcv5dr1i"
+path="res://.godot/imported/log-out.svg-85530e4e6b0167a15c809830d3302ef2.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://Assets/Icons/log-out.svg"
+dest_files=["res://.godot/imported/log-out.svg-85530e4e6b0167a15c809830d3302ef2.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/flumi/Assets/Icons/message-circle-question-mark.svg b/flumi/Assets/Icons/message-circle-question-mark.svg
new file mode 100644
index 0000000..8ec2a50
--- /dev/null
+++ b/flumi/Assets/Icons/message-circle-question-mark.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/flumi/Assets/Icons/message-circle-question-mark.svg.import b/flumi/Assets/Icons/message-circle-question-mark.svg.import
new file mode 100644
index 0000000..d445cfe
--- /dev/null
+++ b/flumi/Assets/Icons/message-circle-question-mark.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://c5pr3tb8rwxb8"
+path="res://.godot/imported/message-circle-question-mark.svg-6e0198922e95be66aacb1420c83ea704.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://Assets/Icons/message-circle-question-mark.svg"
+dest_files=["res://.godot/imported/message-circle-question-mark.svg-6e0198922e95be66aacb1420c83ea704.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/flumi/Assets/Icons/settings.svg b/flumi/Assets/Icons/settings.svg
new file mode 100644
index 0000000..36b6fcf
--- /dev/null
+++ b/flumi/Assets/Icons/settings.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/flumi/Assets/Icons/settings.svg.import b/flumi/Assets/Icons/settings.svg.import
new file mode 100644
index 0000000..d017cce
--- /dev/null
+++ b/flumi/Assets/Icons/settings.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://mjr3nwamrqon"
+path="res://.godot/imported/settings.svg-2aa0f389da6ad0a7e346738ae84fd469.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://Assets/Icons/settings.svg"
+dest_files=["res://.godot/imported/settings.svg-2aa0f389da6ad0a7e346738ae84fd469.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/flumi/Assets/Icons/square.svg b/flumi/Assets/Icons/square.svg
new file mode 100644
index 0000000..e96246e
--- /dev/null
+++ b/flumi/Assets/Icons/square.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/flumi/Assets/Icons/square.svg.import b/flumi/Assets/Icons/square.svg.import
new file mode 100644
index 0000000..da7952d
--- /dev/null
+++ b/flumi/Assets/Icons/square.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://81l2fi381yub"
+path="res://.godot/imported/square.svg-64faf57e7837716ed0ed796cd3df54d8.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://Assets/Icons/square.svg"
+dest_files=["res://.godot/imported/square.svg-64faf57e7837716ed0ed796cd3df54d8.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/flumi/Assets/Icons/star.svg b/flumi/Assets/Icons/star.svg
new file mode 100644
index 0000000..4bdba35
--- /dev/null
+++ b/flumi/Assets/Icons/star.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/flumi/Assets/Icons/star.svg.import b/flumi/Assets/Icons/star.svg.import
new file mode 100644
index 0000000..e8e9a65
--- /dev/null
+++ b/flumi/Assets/Icons/star.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://codete2cbsqo2"
+path="res://.godot/imported/star.svg-680978d494bec0663c353c2873105a07.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://Assets/Icons/star.svg"
+dest_files=["res://.godot/imported/star.svg-680978d494bec0663c353c2873105a07.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/flumi/Assets/Inter-VariableFont_opsz,wght.ttf b/flumi/Assets/Inter-VariableFont_opsz,wght.ttf
new file mode 100644
index 0000000..e31b51e
Binary files /dev/null and b/flumi/Assets/Inter-VariableFont_opsz,wght.ttf differ
diff --git a/flumi/Assets/Inter-VariableFont_opsz,wght.ttf.import b/flumi/Assets/Inter-VariableFont_opsz,wght.ttf.import
new file mode 100644
index 0000000..2cade4f
--- /dev/null
+++ b/flumi/Assets/Inter-VariableFont_opsz,wght.ttf.import
@@ -0,0 +1,35 @@
+[remap]
+
+importer="font_data_dynamic"
+type="FontFile"
+uid="uid://fij84uxfqh4h"
+path="res://.godot/imported/Inter-VariableFont_opsz,wght.ttf-c02350de2ed9338f034e978775e464ff.fontdata"
+
+[deps]
+
+source_file="res://Assets/Inter-VariableFont_opsz,wght.ttf"
+dest_files=["res://.godot/imported/Inter-VariableFont_opsz,wght.ttf-c02350de2ed9338f034e978775e464ff.fontdata"]
+
+[params]
+
+Rendering=null
+antialiasing=1
+generate_mipmaps=false
+disable_embedded_bitmaps=true
+multichannel_signed_distance_field=false
+msdf_pixel_range=8
+msdf_size=48
+allow_system_fallback=true
+force_autohinter=false
+hinting=1
+subpixel_positioning=4
+keep_rounding_remainders=true
+oversampling=0.0
+Fallbacks=null
+fallbacks=[]
+Compress=null
+compress=true
+preload=[]
+language_support={}
+script_support={}
+opentype_features={}
diff --git a/flumi/Scenes/BrowserMenus/history.tscn b/flumi/Scenes/BrowserMenus/history.tscn
new file mode 100644
index 0000000..67fc4c5
--- /dev/null
+++ b/flumi/Scenes/BrowserMenus/history.tscn
@@ -0,0 +1,326 @@
+[gd_scene load_steps=15 format=3 uid="uid://cn24pafwdpb1q"]
+
+[ext_resource type="Texture2D" uid="uid://ctpe0lbehepen" path="res://Assets/gurted.svg" id="1_occ3h"]
+[ext_resource type="Script" uid="uid://ektopbvnhfga" path="res://Scripts/history.gd" id="1_yn8i4"]
+[ext_resource type="PackedScene" uid="uid://3smiker6ni50" path="res://Scenes/BrowserMenus/history_entry.tscn" id="2_a5287"]
+[ext_resource type="Texture2D" uid="uid://gq8g7t4s3ryg" path="res://Assets/Icons/x.svg" id="2_ijpe2"]
+[ext_resource type="Theme" uid="uid://bn6rbmdy60lhr" path="res://Scenes/Styles/BrowserText.tres" id="3_yoadi"]
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ijpe2"]
+content_margin_left = 15.0
+content_margin_top = 5.0
+content_margin_right = 15.0
+content_margin_bottom = 5.0
+bg_color = Color(0.105882, 0.105882, 0.105882, 1)
+corner_radius_top_left = 15
+corner_radius_top_right = 15
+corner_radius_bottom_right = 15
+corner_radius_bottom_left = 15
+
+[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_yn8i4"]
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_a8gu8"]
+bg_color = Color(0.169245, 0.169245, 0.169245, 1)
+border_width_left = 2
+border_width_top = 2
+border_width_right = 2
+border_width_bottom = 2
+border_color = Color(0.247059, 0.466667, 0.807843, 1)
+corner_radius_top_left = 25
+corner_radius_top_right = 25
+corner_radius_bottom_right = 25
+corner_radius_bottom_left = 25
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_31fx5"]
+draw_center = false
+border_width_left = 2
+border_width_top = 2
+border_width_right = 2
+border_width_bottom = 2
+border_color = Color(0.247059, 0.466667, 0.807843, 1)
+corner_radius_top_left = 25
+corner_radius_top_right = 25
+corner_radius_bottom_right = 25
+corner_radius_bottom_left = 25
+
+[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_8gbba"]
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_8gbba"]
+bg_color = Color(0.168627, 0.168627, 0.168627, 1)
+corner_radius_top_left = 15
+corner_radius_top_right = 15
+corner_radius_bottom_right = 15
+corner_radius_bottom_left = 15
+expand_margin_left = 40.0
+
+[sub_resource type="Theme" id="Theme_ijpe2"]
+LineEdit/styles/focus = SubResource("StyleBoxEmpty_8gbba")
+LineEdit/styles/normal = SubResource("StyleBoxFlat_8gbba")
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_a5287"]
+content_margin_left = 10.0
+bg_color = Color(0.219501, 0.219501, 0.219501, 1)
+corner_radius_top_left = 15
+corner_radius_top_right = 15
+corner_radius_bottom_right = 15
+corner_radius_bottom_left = 15
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_yn8i4"]
+content_margin_left = 15.0
+content_margin_top = 15.0
+content_margin_right = 15.0
+content_margin_bottom = 5.0
+bg_color = Color(0.105882, 0.105882, 0.105882, 1)
+corner_radius_top_left = 15
+corner_radius_top_right = 15
+corner_radius_bottom_right = 15
+corner_radius_bottom_left = 15
+
+[node name="History" type="MarginContainer"]
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+theme_override_constants/margin_left = 20
+theme_override_constants/margin_top = 20
+theme_override_constants/margin_right = 20
+script = ExtResource("1_yn8i4")
+
+[node name="Control" type="Control" parent="."]
+custom_minimum_size = Vector2(400, 100)
+layout_mode = 2
+
+[node name="TextureRect" type="TextureRect" parent="Control"]
+layout_mode = 1
+offset_right = 417.0
+offset_bottom = 417.0
+scale = Vector2(0.105, 0.105)
+texture = ExtResource("1_occ3h")
+stretch_mode = 2
+
+[node name="RichTextLabel" type="RichTextLabel" parent="Control"]
+layout_mode = 0
+offset_left = 50.0
+offset_top = 9.0
+offset_right = 339.0
+offset_bottom = 85.0
+theme_override_font_sizes/bold_font_size = 26
+bbcode_enabled = true
+text = "[b]History[/b]"
+
+[node name="Main" type="VBoxContainer" parent="."]
+custom_minimum_size = Vector2(600, 0)
+layout_mode = 2
+size_flags_horizontal = 4
+theme_override_constants/separation = 15
+
+[node name="DeleteMenu" type="PanelContainer" parent="Main"]
+visible = false
+layout_mode = 2
+theme_override_styles/panel = SubResource("StyleBoxFlat_ijpe2")
+
+[node name="HBoxContainer" type="HBoxContainer" parent="Main/DeleteMenu"]
+layout_mode = 2
+
+[node name="CancelButton" type="Button" parent="Main/DeleteMenu/HBoxContainer"]
+layout_mode = 2
+mouse_default_cursor_shape = 2
+theme_override_styles/focus = SubResource("StyleBoxEmpty_yn8i4")
+icon = ExtResource("2_ijpe2")
+flat = true
+icon_alignment = 1
+
+[node name="RichTextLabel" type="RichTextLabel" parent="Main/DeleteMenu/HBoxContainer"]
+custom_minimum_size = Vector2(200, 0)
+layout_mode = 2
+text = "1 selected"
+fit_content = true
+vertical_alignment = 1
+
+[node name="DeleteButton" type="Button" parent="Main/DeleteMenu/HBoxContainer"]
+custom_minimum_size = Vector2(80, 35)
+layout_mode = 2
+size_flags_horizontal = 10
+mouse_default_cursor_shape = 2
+theme = ExtResource("3_yoadi")
+theme_override_colors/font_color = Color(0.781065, 0.858202, 0.977018, 1)
+theme_override_styles/hover = SubResource("StyleBoxFlat_a8gu8")
+theme_override_styles/normal = SubResource("StyleBoxFlat_31fx5")
+text = "Delete"
+
+[node name="LineEdit" type="LineEdit" parent="Main"]
+custom_minimum_size = Vector2(0, 45)
+layout_mode = 2
+size_flags_horizontal = 3
+theme = SubResource("Theme_ijpe2")
+theme_override_styles/normal = SubResource("StyleBoxFlat_a5287")
+placeholder_text = "Search history..."
+caret_blink = true
+
+[node name="HSeparator" type="HSeparator" parent="Main"]
+layout_mode = 2
+
+[node name="PanelContainer2" type="PanelContainer" parent="Main"]
+layout_mode = 2
+size_flags_vertical = 3
+theme_override_styles/panel = SubResource("StyleBoxFlat_yn8i4")
+
+[node name="ScrollContainer" type="ScrollContainer" parent="Main/PanelContainer2"]
+layout_mode = 2
+size_flags_vertical = 3
+
+[node name="HistoryEntryContainer" type="VBoxContainer" parent="Main/PanelContainer2/ScrollContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+alignment = 1
+
+[node name="HistoryEntry" parent="Main/PanelContainer2/ScrollContainer/HistoryEntryContainer" instance=ExtResource("2_a5287")]
+layout_mode = 2
+
+[node name="HistoryEntry2" parent="Main/PanelContainer2/ScrollContainer/HistoryEntryContainer" instance=ExtResource("2_a5287")]
+layout_mode = 2
+
+[node name="HistoryEntry3" parent="Main/PanelContainer2/ScrollContainer/HistoryEntryContainer" instance=ExtResource("2_a5287")]
+layout_mode = 2
+
+[node name="HistoryEntry4" parent="Main/PanelContainer2/ScrollContainer/HistoryEntryContainer" instance=ExtResource("2_a5287")]
+layout_mode = 2
+
+[node name="HistoryEntry5" parent="Main/PanelContainer2/ScrollContainer/HistoryEntryContainer" instance=ExtResource("2_a5287")]
+layout_mode = 2
+
+[node name="HistoryEntry6" parent="Main/PanelContainer2/ScrollContainer/HistoryEntryContainer" instance=ExtResource("2_a5287")]
+layout_mode = 2
+
+[node name="HistoryEntry7" parent="Main/PanelContainer2/ScrollContainer/HistoryEntryContainer" instance=ExtResource("2_a5287")]
+layout_mode = 2
+
+[node name="HistoryEntry8" parent="Main/PanelContainer2/ScrollContainer/HistoryEntryContainer" instance=ExtResource("2_a5287")]
+layout_mode = 2
+
+[node name="HistoryEntry9" parent="Main/PanelContainer2/ScrollContainer/HistoryEntryContainer" instance=ExtResource("2_a5287")]
+layout_mode = 2
+
+[node name="HistoryEntry10" parent="Main/PanelContainer2/ScrollContainer/HistoryEntryContainer" instance=ExtResource("2_a5287")]
+layout_mode = 2
+
+[node name="HistoryEntry11" parent="Main/PanelContainer2/ScrollContainer/HistoryEntryContainer" instance=ExtResource("2_a5287")]
+layout_mode = 2
+
+[node name="HistoryEntry12" parent="Main/PanelContainer2/ScrollContainer/HistoryEntryContainer" instance=ExtResource("2_a5287")]
+layout_mode = 2
+
+[node name="HistoryEntry13" parent="Main/PanelContainer2/ScrollContainer/HistoryEntryContainer" instance=ExtResource("2_a5287")]
+layout_mode = 2
+
+[node name="HistoryEntry14" parent="Main/PanelContainer2/ScrollContainer/HistoryEntryContainer" instance=ExtResource("2_a5287")]
+layout_mode = 2
+
+[node name="HistoryEntry15" parent="Main/PanelContainer2/ScrollContainer/HistoryEntryContainer" instance=ExtResource("2_a5287")]
+layout_mode = 2
+
+[node name="HistoryEntry16" parent="Main/PanelContainer2/ScrollContainer/HistoryEntryContainer" instance=ExtResource("2_a5287")]
+layout_mode = 2
+
+[node name="HistoryEntry17" parent="Main/PanelContainer2/ScrollContainer/HistoryEntryContainer" instance=ExtResource("2_a5287")]
+layout_mode = 2
+
+[node name="HistoryEntry18" parent="Main/PanelContainer2/ScrollContainer/HistoryEntryContainer" instance=ExtResource("2_a5287")]
+layout_mode = 2
+
+[node name="HistoryEntry19" parent="Main/PanelContainer2/ScrollContainer/HistoryEntryContainer" instance=ExtResource("2_a5287")]
+layout_mode = 2
+
+[node name="HistoryEntry20" parent="Main/PanelContainer2/ScrollContainer/HistoryEntryContainer" instance=ExtResource("2_a5287")]
+layout_mode = 2
+
+[node name="HistoryEntry21" parent="Main/PanelContainer2/ScrollContainer/HistoryEntryContainer" instance=ExtResource("2_a5287")]
+layout_mode = 2
+
+[node name="HistoryEntry22" parent="Main/PanelContainer2/ScrollContainer/HistoryEntryContainer" instance=ExtResource("2_a5287")]
+layout_mode = 2
+
+[node name="HistoryEntry23" parent="Main/PanelContainer2/ScrollContainer/HistoryEntryContainer" instance=ExtResource("2_a5287")]
+layout_mode = 2
+
+[node name="HistoryEntry24" parent="Main/PanelContainer2/ScrollContainer/HistoryEntryContainer" instance=ExtResource("2_a5287")]
+layout_mode = 2
+
+[node name="HistoryEntry25" parent="Main/PanelContainer2/ScrollContainer/HistoryEntryContainer" instance=ExtResource("2_a5287")]
+layout_mode = 2
+
+[node name="HistoryEntry26" parent="Main/PanelContainer2/ScrollContainer/HistoryEntryContainer" instance=ExtResource("2_a5287")]
+layout_mode = 2
+
+[node name="HistoryEntry27" parent="Main/PanelContainer2/ScrollContainer/HistoryEntryContainer" instance=ExtResource("2_a5287")]
+layout_mode = 2
+
+[node name="HistoryEntry28" parent="Main/PanelContainer2/ScrollContainer/HistoryEntryContainer" instance=ExtResource("2_a5287")]
+layout_mode = 2
+
+[node name="HistoryEntry29" parent="Main/PanelContainer2/ScrollContainer/HistoryEntryContainer" instance=ExtResource("2_a5287")]
+layout_mode = 2
+
+[node name="HistoryEntry30" parent="Main/PanelContainer2/ScrollContainer/HistoryEntryContainer" instance=ExtResource("2_a5287")]
+layout_mode = 2
+
+[node name="HistoryEntry31" parent="Main/PanelContainer2/ScrollContainer/HistoryEntryContainer" instance=ExtResource("2_a5287")]
+layout_mode = 2
+
+[node name="HistoryEntry32" parent="Main/PanelContainer2/ScrollContainer/HistoryEntryContainer" instance=ExtResource("2_a5287")]
+layout_mode = 2
+
+[node name="HistoryEntry33" parent="Main/PanelContainer2/ScrollContainer/HistoryEntryContainer" instance=ExtResource("2_a5287")]
+layout_mode = 2
+
+[node name="HistoryEntry34" parent="Main/PanelContainer2/ScrollContainer/HistoryEntryContainer" instance=ExtResource("2_a5287")]
+layout_mode = 2
+
+[node name="HistoryEntry35" parent="Main/PanelContainer2/ScrollContainer/HistoryEntryContainer" instance=ExtResource("2_a5287")]
+layout_mode = 2
+
+[node name="HistoryEntry36" parent="Main/PanelContainer2/ScrollContainer/HistoryEntryContainer" instance=ExtResource("2_a5287")]
+layout_mode = 2
+
+[node name="HistoryEntry37" parent="Main/PanelContainer2/ScrollContainer/HistoryEntryContainer" instance=ExtResource("2_a5287")]
+layout_mode = 2
+
+[node name="HistoryEntry38" parent="Main/PanelContainer2/ScrollContainer/HistoryEntryContainer" instance=ExtResource("2_a5287")]
+layout_mode = 2
+
+[node name="HistoryEntry39" parent="Main/PanelContainer2/ScrollContainer/HistoryEntryContainer" instance=ExtResource("2_a5287")]
+layout_mode = 2
+
+[node name="HistoryEntry40" parent="Main/PanelContainer2/ScrollContainer/HistoryEntryContainer" instance=ExtResource("2_a5287")]
+layout_mode = 2
+
+[node name="HistoryEntry41" parent="Main/PanelContainer2/ScrollContainer/HistoryEntryContainer" instance=ExtResource("2_a5287")]
+layout_mode = 2
+
+[node name="HistoryEntry42" parent="Main/PanelContainer2/ScrollContainer/HistoryEntryContainer" instance=ExtResource("2_a5287")]
+layout_mode = 2
+
+[node name="HistoryEntry43" parent="Main/PanelContainer2/ScrollContainer/HistoryEntryContainer" instance=ExtResource("2_a5287")]
+layout_mode = 2
+
+[node name="HistoryEntry44" parent="Main/PanelContainer2/ScrollContainer/HistoryEntryContainer" instance=ExtResource("2_a5287")]
+layout_mode = 2
+
+[node name="HistoryEntry45" parent="Main/PanelContainer2/ScrollContainer/HistoryEntryContainer" instance=ExtResource("2_a5287")]
+layout_mode = 2
+
+[node name="HistoryEntry46" parent="Main/PanelContainer2/ScrollContainer/HistoryEntryContainer" instance=ExtResource("2_a5287")]
+layout_mode = 2
+
+[node name="HistoryEntry47" parent="Main/PanelContainer2/ScrollContainer/HistoryEntryContainer" instance=ExtResource("2_a5287")]
+layout_mode = 2
+
+[node name="HistoryEntry48" parent="Main/PanelContainer2/ScrollContainer/HistoryEntryContainer" instance=ExtResource("2_a5287")]
+layout_mode = 2
+
+[node name="HistoryEntry49" parent="Main/PanelContainer2/ScrollContainer/HistoryEntryContainer" instance=ExtResource("2_a5287")]
+layout_mode = 2
+
+[connection signal="pressed" from="Main/DeleteMenu/HBoxContainer/CancelButton" to="." method="_on_cancel_button_pressed"]
diff --git a/flumi/Scenes/BrowserMenus/history_entry.tscn b/flumi/Scenes/BrowserMenus/history_entry.tscn
new file mode 100644
index 0000000..6cc6f91
--- /dev/null
+++ b/flumi/Scenes/BrowserMenus/history_entry.tscn
@@ -0,0 +1,42 @@
+[gd_scene load_steps=5 format=3 uid="uid://3smiker6ni50"]
+
+[ext_resource type="Theme" uid="uid://bn6rbmdy60lhr" path="res://Scenes/Styles/BrowserText.tres" id="1_4plhl"]
+[ext_resource type="Script" uid="uid://bw5pr4wrf780h" path="res://Scripts/history_entry.gd" id="1_h5c6k"]
+[ext_resource type="Texture2D" uid="uid://blskvfjswul1d" path="res://Assets/Icons/checkbox_white.svg" id="2_h5c6k"]
+[ext_resource type="Texture2D" uid="uid://bqpx2lgo0yecb" path="res://Assets/Icons/globe.svg" id="2_k4hqm"]
+
+[node name="HistoryEntry" type="HBoxContainer"]
+theme_override_constants/separation = 10
+script = ExtResource("1_h5c6k")
+
+[node name="CheckBox" type="CheckBox" parent="."]
+layout_mode = 2
+theme = ExtResource("1_4plhl")
+theme_override_icons/unchecked = ExtResource("2_h5c6k")
+flat = true
+
+[node name="RichTextLabel" type="RichTextLabel" parent="."]
+custom_minimum_size = Vector2(60, 0)
+layout_mode = 2
+text = "2:00PM"
+fit_content = true
+horizontal_alignment = 1
+vertical_alignment = 1
+
+[node name="Spacer" type="Control" parent="."]
+custom_minimum_size = Vector2(50, 0)
+layout_mode = 2
+
+[node name="TextureRect" type="TextureRect" parent="."]
+custom_minimum_size = Vector2(24, 24)
+layout_mode = 2
+texture = ExtResource("2_k4hqm")
+stretch_mode = 3
+
+[node name="RichTextLabel2" type="RichTextLabel" parent="."]
+custom_minimum_size = Vector2(350, 0)
+layout_mode = 2
+text = "Selection - Google Fonts"
+vertical_alignment = 1
+
+[connection signal="toggled" from="CheckBox" to="." method="_on_check_box_toggled"]
diff --git a/flumi/Scenes/Styles/BrowserText.tres b/flumi/Scenes/Styles/BrowserText.tres
index f0b4990..f179456 100644
--- a/flumi/Scenes/Styles/BrowserText.tres
+++ b/flumi/Scenes/Styles/BrowserText.tres
@@ -112,10 +112,10 @@ content_margin_top = 15.0
content_margin_right = 15.0
content_margin_bottom = 15.0
bg_color = Color(0.168627, 0.168627, 0.168627, 1)
-corner_radius_top_left = 15
-corner_radius_top_right = 15
-corner_radius_bottom_right = 15
-corner_radius_bottom_left = 15
+corner_radius_top_left = 10
+corner_radius_top_right = 10
+corner_radius_bottom_right = 10
+corner_radius_bottom_left = 10
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ya683"]
content_margin_left = 15.0
diff --git a/flumi/Scenes/Tags/p.tscn b/flumi/Scenes/Tags/p.tscn
index cbece4c..a529b6e 100644
--- a/flumi/Scenes/Tags/p.tscn
+++ b/flumi/Scenes/Tags/p.tscn
@@ -8,6 +8,7 @@ anchors_preset = 10
anchor_right = 1.0
offset_bottom = 19.0
grow_horizontal = 2
+size_flags_horizontal = 3
focus_mode = 2
mouse_default_cursor_shape = 1
theme = ExtResource("2_1glvj")
@@ -15,5 +16,6 @@ theme_override_colors/default_color = Color(0, 0, 0, 1)
bbcode_enabled = true
text = "Placeholder"
fit_content = true
+vertical_alignment = 1
selection_enabled = true
script = ExtResource("1_pnbfg")
diff --git a/flumi/Scenes/main.tscn b/flumi/Scenes/main.tscn
index a72adce..1e0c479 100644
--- a/flumi/Scenes/main.tscn
+++ b/flumi/Scenes/main.tscn
@@ -1,4 +1,4 @@
-[gd_scene load_steps=27 format=3 uid="uid://bytm7bt2s4ak8"]
+[gd_scene load_steps=40 format=3 uid="uid://bytm7bt2s4ak8"]
[ext_resource type="Script" uid="uid://bg5iqnwic1rio" path="res://Scripts/main.gd" id="1_8q3xr"]
[ext_resource type="Texture2D" uid="uid://df1m4j7uxi63v" path="res://Assets/Icons/chevron-down.svg" id="2_6bp64"]
@@ -9,8 +9,21 @@
[ext_resource type="PackedScene" uid="uid://sqhcxhcre081" path="res://Scenes/Tab.tscn" id="4_344ge"]
[ext_resource type="Texture2D" uid="uid://cu4hjoba6etf" path="res://Assets/Icons/rotate-cw.svg" id="5_344ge"]
[ext_resource type="Texture2D" uid="uid://cehbtwq6gq0cn" path="res://Assets/Icons/plus.svg" id="5_ynf5e"]
+[ext_resource type="Texture2D" uid="uid://cklatjc4m38dy" path="res://Assets/Icons/ellipsis-vertical.svg" id="10_6iyac"]
[ext_resource type="Script" uid="uid://bgqglerkcylxx" path="res://addons/SmoothScroll/SmoothScrollContainer.gd" id="10_d1ilt"]
[ext_resource type="Script" uid="uid://b7h0k2h2qwlqv" path="res://addons/SmoothScroll/scroll_damper/expo_scroll_damper.gd" id="11_6iyac"]
+[ext_resource type="Theme" uid="uid://bn6rbmdy60lhr" path="res://Scenes/Styles/BrowserText.tres" id="11_ee4r6"]
+[ext_resource type="Script" uid="uid://vjjhljlftlbk" path="res://Scripts/OptionButton.gd" id="11_gt3je"]
+[ext_resource type="Texture2D" uid="uid://du24f6em2nqwq" path="res://Assets/Icons/external-link.svg" id="12_gt3je"]
+[ext_resource type="Texture2D" uid="uid://81l2fi381yub" path="res://Assets/Icons/square.svg" id="13_3pmx8"]
+[ext_resource type="Texture2D" uid="uid://dwxquqmmd6dqx" path="res://Assets/Icons/hat-glasses.svg" id="14_u50mg"]
+[ext_resource type="Texture2D" uid="uid://bcaoarwrwqbby" path="res://Assets/Icons/history.svg" id="15_cbgmd"]
+[ext_resource type="Texture2D" uid="uid://cbwitcygwoqdo" path="res://Assets/Icons/download.svg" id="16_1w6v2"]
+[ext_resource type="Texture2D" uid="uid://codete2cbsqo2" path="res://Assets/Icons/star.svg" id="17_ueoa1"]
+[ext_resource type="Texture2D" uid="uid://c5pr3tb8rwxb8" path="res://Assets/Icons/message-circle-question-mark.svg" id="18_6vcvc"]
+[ext_resource type="Texture2D" uid="uid://mjr3nwamrqon" path="res://Assets/Icons/settings.svg" id="19_7k868"]
+[ext_resource type="Texture2D" uid="uid://cilwaxcv5dr1i" path="res://Assets/Icons/log-out.svg" id="20_hpc6h"]
+[ext_resource type="PackedScene" uid="uid://cn24pafwdpb1q" path="res://Scenes/BrowserMenus/history.tscn" id="24_3pmx8"]
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_344ge"]
@@ -49,9 +62,9 @@ corner_radius_bottom_left = 50
bg_color = Color(0.6, 0.6, 0.6, 0)
draw_center = false
-[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_8gbba"]
+[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_u50mg"]
-[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_8gbba"]
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_cbgmd"]
bg_color = Color(0.168627, 0.168627, 0.168627, 1)
corner_radius_top_left = 15
corner_radius_top_right = 15
@@ -60,8 +73,8 @@ corner_radius_bottom_left = 15
expand_margin_left = 40.0
[sub_resource type="Theme" id="Theme_jjvhh"]
-LineEdit/styles/focus = SubResource("StyleBoxEmpty_8gbba")
-LineEdit/styles/normal = SubResource("StyleBoxFlat_8gbba")
+LineEdit/styles/focus = SubResource("StyleBoxEmpty_u50mg")
+LineEdit/styles/normal = SubResource("StyleBoxFlat_cbgmd")
[sub_resource type="Resource" id="Resource_fdnlq"]
script = ExtResource("11_6iyac")
@@ -200,6 +213,59 @@ scale = Vector2(0.85, 0.85)
texture = ExtResource("3_8gbba")
stretch_mode = 5
+[node name="OptionsButton" type="Button" parent="VBoxContainer/HBoxContainer"]
+custom_minimum_size = Vector2(45, 0)
+layout_mode = 2
+theme_override_styles/focus = SubResource("StyleBoxEmpty_d1ilt")
+theme_override_styles/hover = SubResource("StyleBoxFlat_fdnlq")
+theme_override_styles/pressed = SubResource("StyleBoxFlat_d1ilt")
+theme_override_styles/normal = SubResource("StyleBoxFlat_d1ilt")
+icon = ExtResource("10_6iyac")
+icon_alignment = 1
+script = ExtResource("11_gt3je")
+
+[node name="OptionsMenu" type="PopupMenu" parent="VBoxContainer/HBoxContainer/OptionsButton"]
+unique_name_in_owner = true
+position = Vector2i(1510, 125)
+size = Vector2i(408, 360)
+theme = ExtResource("11_ee4r6")
+theme_override_constants/v_separation = 10
+item_count = 11
+item_0/text = "New tab (CTRL T)"
+item_0/icon = ExtResource("12_gt3je")
+item_0/id = 0
+item_1/text = "New window (CTRL N)"
+item_1/icon = ExtResource("13_3pmx8")
+item_1/id = 1
+item_2/text = "New Incognito Window (CTRL SHIFT N)"
+item_2/icon = ExtResource("14_u50mg")
+item_2/id = 2
+item_3/id = 3
+item_3/separator = true
+item_4/text = "History (CTRL H)"
+item_4/icon = ExtResource("15_cbgmd")
+item_4/id = 4
+item_5/text = "Downloads (CTRL J)"
+item_5/icon = ExtResource("16_1w6v2")
+item_5/id = 5
+item_6/text = "Bookmarks (CTRL SHIFT B)"
+item_6/icon = ExtResource("17_ueoa1")
+item_6/id = 7
+item_7/id = 7
+item_7/separator = true
+item_8/text = "Help"
+item_8/icon = ExtResource("18_6vcvc")
+item_8/id = 8
+item_9/text = "Settings"
+item_9/icon = ExtResource("19_7k868")
+item_9/id = 9
+item_10/text = "Exit"
+item_10/icon = ExtResource("20_hpc6h")
+item_10/id = 10
+
+[node name="Control2" type="Control" parent="VBoxContainer/HBoxContainer"]
+layout_mode = 2
+
[node name="Spacer3" type="Control" parent="VBoxContainer"]
custom_minimum_size = Vector2(0, 2)
layout_mode = 2
@@ -216,12 +282,17 @@ metadata/_custom_type_script = "uid://bgqglerkcylxx"
[node name="WebsiteContainer" type="VBoxContainer" parent="VBoxContainer/ScrollContainer"]
unique_name_in_owner = true
-custom_minimum_size = Vector2(200, 200)
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
theme_override_constants/separation = 22
+[node name="HistoryContainer" parent="VBoxContainer/ScrollContainer" instance=ExtResource("24_3pmx8")]
+visible = false
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+
[node name="WebsiteBackground" type="Panel" parent="."]
unique_name_in_owner = true
z_index = -1
@@ -259,3 +330,8 @@ mouse_filter = 2
theme_override_styles/panel = SubResource("StyleBoxFlat_21xkr")
[connection signal="pressed" from="VBoxContainer/TabContainer/NewTabButton" to="VBoxContainer/TabContainer" method="_on_new_tab_button_pressed"]
+[connection signal="focus_entered" from="VBoxContainer/HBoxContainer/LineEdit" to="." method="_on_search_focus_entered"]
+[connection signal="focus_exited" from="VBoxContainer/HBoxContainer/LineEdit" to="." method="_on_search_focus_exited"]
+[connection signal="text_submitted" from="VBoxContainer/HBoxContainer/LineEdit" to="." method="_on_search_submitted"]
+[connection signal="pressed" from="VBoxContainer/HBoxContainer/OptionsButton" to="VBoxContainer/HBoxContainer/OptionsButton" method="_on_pressed"]
+[connection signal="id_pressed" from="VBoxContainer/HBoxContainer/OptionsButton/OptionsMenu" to="VBoxContainer/HBoxContainer/OptionsButton" method="_on_options_menu_id_pressed"]
diff --git a/flumi/Scripts/B9/HTMLParser.gd b/flumi/Scripts/B9/HTMLParser.gd
index 3d21cba..50bf971 100644
--- a/flumi/Scripts/B9/HTMLParser.gd
+++ b/flumi/Scripts/B9/HTMLParser.gd
@@ -151,7 +151,7 @@ func get_element_styles_with_inheritance(element: HTMLElement, event: String = "
styles[property] = inline_parsed[property]
# Inherit certain properties from parent elements
- var inheritable_properties = ["width", "height", "font-size", "color", "font-family", "cursor"]
+ var inheritable_properties = ["width", "height", "font-size", "color", "font-family", "cursor", "font-bold", "font-italic", "underline"]
var parent_element = element.parent
while parent_element:
var parent_styles = get_element_styles_internal(parent_element, event)
diff --git a/flumi/Scripts/B9/Lua.gd b/flumi/Scripts/B9/Lua.gd
index 843e46c..5f786d2 100644
--- a/flumi/Scripts/B9/Lua.gd
+++ b/flumi/Scripts/B9/Lua.gd
@@ -28,6 +28,7 @@ func _init():
timeout_manager = LuaTimeoutManager.new()
threaded_vm = ThreadedLuaVM.new()
threaded_vm.script_completed.connect(_on_threaded_script_completed)
+ threaded_vm.script_error.connect(func(e): print(e))
threaded_vm.dom_operation_request.connect(_handle_dom_operation)
threaded_vm.print_output.connect(_on_print_output)
diff --git a/flumi/Scripts/Constants.gd b/flumi/Scripts/Constants.gd
index db50667..db9fb60 100644
--- a/flumi/Scripts/Constants.gd
+++ b/flumi/Scripts/Constants.gd
@@ -2794,4 +2794,108 @@ audio.play()
# Set the active HTML content to use the audio demo
func _ready():
- HTML_CONTENT = HTML_CONTENT_AUDIO_TEST
+ HTML_CONTENT = """
+ Login
+
+
+
+
+
+
+
+
+
+
+
+
+
+""".to_utf8_buffer()
diff --git a/flumi/Scripts/GurtProtocol.gd b/flumi/Scripts/GurtProtocol.gd
new file mode 100644
index 0000000..5407148
--- /dev/null
+++ b/flumi/Scripts/GurtProtocol.gd
@@ -0,0 +1,198 @@
+extends RefCounted
+class_name GurtProtocol
+
+const DNS_API_URL = "http://localhost:8080"
+
+static func is_gurt_domain(url: String) -> bool:
+ if url.begins_with("gurt://"):
+ return true
+
+ var parts = url.split(".")
+ return parts.size() == 2 and not url.contains("://")
+
+static func parse_gurt_domain(url: String) -> Dictionary:
+ print("Parsing URL: ", url)
+
+ var domain_part = url
+
+ if url.begins_with("gurt://"):
+ domain_part = url.substr(7) # Remove "gurt://"
+
+ var parts = domain_part.split(".")
+ if parts.size() != 2:
+ print("Invalid domain format: ", domain_part)
+ return {}
+
+ print("Parsed domain - name: ", parts[0], ", tld: ", parts[1])
+ return {
+ "name": parts[0],
+ "tld": parts[1],
+ "display_url": domain_part
+ }
+
+static func fetch_domain_info(name: String, tld: String) -> Dictionary:
+ print("Fetching domain info for: ", name, ".", tld)
+
+ var http_request = HTTPRequest.new()
+ var tree = Engine.get_main_loop()
+ tree.current_scene.add_child(http_request)
+
+ http_request.timeout = 5.0
+
+ var url = DNS_API_URL + "/domain/" + name + "/" + tld
+ print("DNS API URL: ", url)
+
+ var error = http_request.request(url)
+
+ if error != OK:
+ print("HTTP request failed with error: ", error)
+ http_request.queue_free()
+ return {"error": "Failed to make DNS request"}
+
+ var response = await http_request.request_completed
+ http_request.queue_free()
+
+ if response[1] == 0 and response[3].size() == 0:
+ print("DNS API request timed out")
+ return {"error": "DNS server is not responding"}
+
+ var http_code = response[1]
+ var body = response[3]
+
+ print("DNS API response code: ", http_code)
+ print("DNS API response body: ", body.get_string_from_utf8())
+
+ if http_code != 200:
+ return {"error": "Domain not found or not approved"}
+
+ var json = JSON.new()
+ var parse_result = json.parse(body.get_string_from_utf8())
+
+ if parse_result != OK:
+ print("JSON parse error: ", parse_result)
+ return {"error": "Invalid JSON response from DNS server"}
+
+ print("Domain info retrieved: ", json.data)
+ return json.data
+
+static func fetch_index_html(ip: String) -> String:
+ print("Fetching index.html from IP: ", ip)
+
+ var http_request = HTTPRequest.new()
+ var tree = Engine.get_main_loop()
+ tree.current_scene.add_child(http_request)
+
+ http_request.timeout = 5.0
+
+ var url = "http://" + ip + "/index.html"
+ print("Fetching from URL: ", url)
+
+ var error = http_request.request(url)
+
+ if error != OK:
+ print("HTTP request to IP failed with error: ", error)
+ http_request.queue_free()
+ return ""
+
+ var response = await http_request.request_completed
+ http_request.queue_free()
+
+ if response[1] == 0 and response[3].size() == 0:
+ print("Index.html request timed out")
+ return ""
+
+ var http_code = response[1]
+ var body = response[3]
+
+ print("IP response code: ", http_code)
+
+ if http_code != 200:
+ print("Failed to fetch index.html, HTTP code: ", http_code)
+ return ""
+
+ var html_content = body.get_string_from_utf8()
+ print("Successfully fetched HTML content (", html_content.length(), " characters)")
+ return html_content
+
+static func handle_gurt_domain(url: String) -> Dictionary:
+ print("Handling GURT domain: ", url)
+
+ var parsed = parse_gurt_domain(url)
+ if parsed.is_empty():
+ return {"error": "Invalid domain format. Use: domain.tld", "html": create_error_page("Invalid domain format. Use: domain.tld")}
+
+ var domain_info = await fetch_domain_info(parsed.name, parsed.tld)
+ if domain_info.has("error"):
+ return {"error": domain_info.error, "html": create_error_page(domain_info.error)}
+
+ var html_content = await fetch_index_html(domain_info.ip)
+ if html_content.is_empty():
+ var error_msg = "Failed to fetch index.html from " + domain_info.ip
+ return {"error": error_msg, "html": create_error_page(error_msg)}
+
+ return {"html": html_content, "display_url": parsed.display_url}
+
+static func get_error_type(error_message: String) -> Dictionary:
+ if "DNS server is not responding" in error_message or "Domain not found" in error_message:
+ return {"code": "ERR_NAME_NOT_RESOLVED", "title": "This site can't be reached", "icon": "🌐"}
+ elif "timeout" in error_message.to_lower() or "timed out" in error_message.to_lower():
+ return {"code": "ERR_CONNECTION_TIMED_OUT", "title": "This site can't be reached", "icon": "⏰"}
+ elif "Failed to fetch" in error_message or "HTTP request failed" in error_message:
+ return {"code": "ERR_CONNECTION_REFUSED", "title": "This site can't be reached", "icon": "🚫"}
+ elif "Invalid domain format" in error_message:
+ return {"code": "ERR_INVALID_URL", "title": "This page isn't working", "icon": "⚠️"}
+ else:
+ return {"code": "ERR_UNKNOWN", "title": "Something went wrong", "icon": "❌"}
+
+static func create_error_page(error_message: String) -> String:
+ var error_info = get_error_type(error_message)
+
+ return """
+ """ + error_info.title + """ - GURT
+
+
+
+
+
+
+
+
""" + error_info.icon + """
+
+
""" + error_info.title + """
+
+
""" + error_message + """
+
+
""" + error_info.code + """
+
+
+
Try:
+
+ Checking if the domain is correctly registered
+ Verifying your DNS server is running
+ Checking your internet connection
+
+
+
+
Reload
+
+"""
diff --git a/flumi/Scripts/GurtProtocol.gd.uid b/flumi/Scripts/GurtProtocol.gd.uid
new file mode 100644
index 0000000..858789e
--- /dev/null
+++ b/flumi/Scripts/GurtProtocol.gd.uid
@@ -0,0 +1 @@
+uid://clhivwjs3eujk
diff --git a/flumi/Scripts/OptionButton.gd b/flumi/Scripts/OptionButton.gd
new file mode 100644
index 0000000..258aeb6
--- /dev/null
+++ b/flumi/Scripts/OptionButton.gd
@@ -0,0 +1,18 @@
+extends Button
+
+@onready var tab_container: TabManager = $"../../TabContainer"
+@onready var website_background: Panel = %WebsiteBackground
+
+func _on_pressed() -> void:
+ %OptionsMenu.show()
+
+func _on_options_menu_id_pressed(id: int) -> void:
+ if id == 0: # new tab
+ tab_container.create_tab()
+ if id == 1: # new window
+ OS.create_process(OS.get_executable_path(), [])
+ if id == 2: # new ingonito window
+ # TODO: handle incognito
+ OS.create_process(OS.get_executable_path(), ["--incognito"])
+ if id == 4: # history
+ website_background.modulate = Constants.SECONDARY_COLOR
diff --git a/flumi/Scripts/OptionButton.gd.uid b/flumi/Scripts/OptionButton.gd.uid
new file mode 100644
index 0000000..b14d10a
--- /dev/null
+++ b/flumi/Scripts/OptionButton.gd.uid
@@ -0,0 +1 @@
+uid://vjjhljlftlbk
diff --git a/flumi/Scripts/StyleManager.gd b/flumi/Scripts/StyleManager.gd
index bd21331..ba8a069 100644
--- a/flumi/Scripts/StyleManager.gd
+++ b/flumi/Scripts/StyleManager.gd
@@ -318,16 +318,23 @@ static func apply_margin_wrapper(node: Control, styles: Dictionary) -> Control:
var margin_container = MarginContainer.new()
margin_container.name = "MarginWrapper_" + node.name
- # Copy size flags from the original node
- margin_container.size_flags_horizontal = node.size_flags_horizontal
- margin_container.size_flags_vertical = node.size_flags_vertical
+ var has_explicit_width = styles.has("width")
+ var has_explicit_height = styles.has("height")
+
+ if has_explicit_width:
+ margin_container.size_flags_horizontal = node.size_flags_horizontal
+ else:
+ margin_container.size_flags_horizontal = node.size_flags_horizontal
+ node.size_flags_horizontal = Control.SIZE_EXPAND_FILL
+
+ if has_explicit_height:
+ margin_container.size_flags_vertical = node.size_flags_vertical
+ else:
+ margin_container.size_flags_vertical = node.size_flags_vertical
+ node.size_flags_vertical = Control.SIZE_EXPAND_FILL
apply_margin_styles_to_container(margin_container, styles)
- # Reset the original node's size flags since they're now handled by the wrapper
- node.size_flags_horizontal = Control.SIZE_EXPAND_FILL
- node.size_flags_vertical = Control.SIZE_EXPAND_FILL
-
# Handle reparenting properly
var original_parent = node.get_parent()
if original_parent:
@@ -542,9 +549,22 @@ static func parse_radius(radius_str: String) -> int:
static func apply_font_to_label(label: RichTextLabel, font_resource: Font) -> void:
label.add_theme_font_override("normal_font", font_resource)
- label.add_theme_font_override("bold_font", font_resource)
- label.add_theme_font_override("italics_font", font_resource)
- label.add_theme_font_override("bold_italics_font", font_resource)
+
+ var bold_font = SystemFont.new()
+ bold_font.font_names = font_resource.font_names if font_resource is SystemFont else ["Arial"]
+ bold_font.font_weight = 700 # Bold weight
+ label.add_theme_font_override("bold_font", bold_font)
+
+ var italic_font = SystemFont.new()
+ italic_font.font_names = font_resource.font_names if font_resource is SystemFont else ["Arial"]
+ italic_font.font_italic = true
+ label.add_theme_font_override("italics_font", italic_font)
+
+ var bold_italic_font = SystemFont.new()
+ bold_italic_font.font_names = font_resource.font_names if font_resource is SystemFont else ["Arial"]
+ bold_italic_font.font_weight = 700 # Bold weight
+ bold_italic_font.font_italic = true
+ label.add_theme_font_override("bold_italics_font", bold_italic_font)
static func apply_font_to_button(button: Button, styles: Dictionary) -> void:
if styles.has("font-family"):
diff --git a/flumi/Scripts/Tab.gd b/flumi/Scripts/Tab.gd
index 8b2040b..62181fb 100644
--- a/flumi/Scripts/Tab.gd
+++ b/flumi/Scripts/Tab.gd
@@ -47,6 +47,10 @@ func set_icon(new_icon: Texture) -> void:
icon.rotation = 0
func update_icon_from_url(icon_url: String) -> void:
+ if icon_url.is_empty():
+ stop_loading()
+ return
+
const LOADER_CIRCLE = preload("res://Assets/Icons/loader-circle.svg")
loading_tween = create_tween()
@@ -68,9 +72,7 @@ func update_icon_from_url(icon_url: String) -> void:
# Only update if tab still exists
if is_instance_valid(self):
set_icon(icon_resource)
- if loading_tween:
- loading_tween.kill()
- loading_tween = null
+ stop_loading()
func _on_button_mouse_entered() -> void:
mouse_over_tab = true
@@ -82,6 +84,27 @@ func _on_button_mouse_exited() -> void:
if is_active: return
gradient_texture.texture = TAB_GRADIENT_DEFAULT
+func start_loading() -> void:
+ const LOADER_CIRCLE = preload("res://Assets/Icons/loader-circle.svg")
+
+ stop_loading()
+
+ loading_tween = create_tween()
+ set_icon(LOADER_CIRCLE)
+ loading_tween.set_loops()
+ icon.pivot_offset = Vector2(11.5, 11.5)
+ loading_tween.tween_method(func(angle):
+ if !is_instance_valid(icon):
+ if loading_tween: loading_tween.kill()
+ return
+ icon.rotation = angle
+ , 0.0, TAU, 1.0)
+
+func stop_loading() -> void:
+ if loading_tween:
+ loading_tween.kill()
+ loading_tween = null
+
func _exit_tree():
if loading_tween:
loading_tween.kill()
diff --git a/flumi/Scripts/Tags/button.gd b/flumi/Scripts/Tags/button.gd
index 8f768fb..02a817d 100644
--- a/flumi/Scripts/Tags/button.gd
+++ b/flumi/Scripts/Tags/button.gd
@@ -189,10 +189,23 @@ func apply_padding_to_stylebox(style_box: StyleBoxFlat, styles: Dictionary) -> v
func apply_size_and_flags(ctrl: Control, width: Variant, height: Variant) -> void:
if width != null or height != null:
- ctrl.custom_minimum_size = Vector2(
- width if width != null else 0,
- height if height != null else 0
- )
+ var new_width = 0
+ var new_height = 0
+
+ if width != null:
+ if SizingUtils.is_percentage(width):
+ new_width = SizingUtils.calculate_percentage_size(width, SizingUtils.DEFAULT_VIEWPORT_WIDTH)
+ else:
+ new_width = width
+
+ if height != null:
+ if SizingUtils.is_percentage(height):
+ new_height = SizingUtils.calculate_percentage_size(height, SizingUtils.DEFAULT_VIEWPORT_HEIGHT)
+ else:
+ new_height = height
+
+ ctrl.custom_minimum_size = Vector2(new_width, new_height)
+
if width != null:
ctrl.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
if height != null:
diff --git a/flumi/Scripts/Tags/input.gd b/flumi/Scripts/Tags/input.gd
index 0ddeca0..69de921 100644
--- a/flumi/Scripts/Tags/input.gd
+++ b/flumi/Scripts/Tags/input.gd
@@ -404,10 +404,22 @@ func apply_input_styles(element: HTMLParser.HTMLElement, parser: HTMLParser) ->
if active_child:
if width or height:
# Explicit sizing from CSS
- var new_child_size = Vector2(
- width if width else active_child.custom_minimum_size.x,
- height if height else max(active_child.custom_minimum_size.y, active_child.size.y)
- )
+ var new_width = active_child.custom_minimum_size.x
+ var new_height = max(active_child.custom_minimum_size.y, active_child.size.y)
+
+ if width:
+ if SizingUtils.is_percentage(width):
+ new_width = SizingUtils.calculate_percentage_size(width, SizingUtils.DEFAULT_VIEWPORT_WIDTH)
+ else:
+ new_width = width
+
+ if height:
+ if SizingUtils.is_percentage(height):
+ new_height = SizingUtils.calculate_percentage_size(height, SizingUtils.DEFAULT_VIEWPORT_HEIGHT)
+ else:
+ new_height = height
+
+ var new_child_size = Vector2(new_width, new_height)
active_child.custom_minimum_size = new_child_size
diff --git a/flumi/Scripts/Tags/p.gd b/flumi/Scripts/Tags/p.gd
index 0934f41..2c53f53 100644
--- a/flumi/Scripts/Tags/p.gd
+++ b/flumi/Scripts/Tags/p.gd
@@ -7,9 +7,38 @@ func init(element: HTMLParser.HTMLElement, parser: HTMLParser) -> void:
# Allow mouse events to pass through to parent containers for hover effects while keeping text selection
mouse_filter = Control.MOUSE_FILTER_PASS
- # NOTE: estimate width/height because FlexContainer removes our anchor preset (sets 0 width)
- var plain_text = element.get_collapsed_text()
- var estimated_height = 30
- var estimated_width = min(200, max(100, plain_text.length() * 12))
+ autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
- custom_minimum_size = Vector2(estimated_width, estimated_height)
+ call_deferred("_auto_resize_to_content")
+
+ size_flags_horizontal = Control.SIZE_EXPAND_FILL
+ set_anchors_and_offsets_preset(Control.PRESET_TOP_WIDE)
+
+func _auto_resize_to_content():
+ if not is_inside_tree():
+ await tree_entered
+
+ var min_width = 20
+ var max_width = 800
+ var min_height = 30
+
+ fit_content = true
+
+ var original_autowrap = autowrap_mode
+ autowrap_mode = TextServer.AUTOWRAP_OFF
+
+ await get_tree().process_frame
+
+ var natural_width = size.x
+ var desired_width = clampf(natural_width, min_width, max_width)
+
+ autowrap_mode = original_autowrap
+
+ await get_tree().process_frame
+
+ var content_height = get_content_height()
+ var explicit_height = custom_minimum_size.y if custom_minimum_size.y > 0 else null
+ var final_height = explicit_height if explicit_height != null else max(content_height, min_height)
+ custom_minimum_size = Vector2(desired_width, final_height)
+
+ queue_redraw()
diff --git a/flumi/Scripts/history.gd b/flumi/Scripts/history.gd
new file mode 100644
index 0000000..d45676c
--- /dev/null
+++ b/flumi/Scripts/history.gd
@@ -0,0 +1,39 @@
+extends MarginContainer
+
+@onready var history_entry_container: VBoxContainer = $Main/PanelContainer2/ScrollContainer/HistoryEntryContainer
+@onready var delete_menu: PanelContainer = $Main/DeleteMenu
+@onready var line_edit: LineEdit = $Main/LineEdit
+@onready var entries_label: RichTextLabel = $Main/DeleteMenu/HBoxContainer/RichTextLabel
+@onready var cancel_button: Button = $Main/DeleteMenu/HBoxContainer/CancelButton
+
+var toggled_entries = []
+
+func _ready():
+ for entry in history_entry_container.get_children():
+ entry.connect("checkbox_toggle", history_toggle.bind(entry))
+
+func history_toggle(toggled: bool, entry) -> void:
+ print('toggling ', entry, ' to :', toggled)
+ if toggled:
+ toggled_entries.append(entry)
+ else:
+ toggled_entries.remove_at(toggled_entries.find(entry))
+
+ entries_label.text = str(toggled_entries.size()) + " selected"
+
+ if toggled_entries.size() != 0:
+ delete_menu.show()
+ line_edit.hide()
+ else:
+ delete_menu.hide()
+ line_edit.show()
+
+func _on_cancel_button_pressed() -> void:
+ var entries_to_reset = toggled_entries.duplicate()
+ toggled_entries.clear()
+
+ for entry in entries_to_reset:
+ entry.reset()
+
+ delete_menu.hide()
+ line_edit.show()
diff --git a/flumi/Scripts/history.gd.uid b/flumi/Scripts/history.gd.uid
new file mode 100644
index 0000000..f075c10
--- /dev/null
+++ b/flumi/Scripts/history.gd.uid
@@ -0,0 +1 @@
+uid://ektopbvnhfga
diff --git a/flumi/Scripts/history_entry.gd b/flumi/Scripts/history_entry.gd
new file mode 100644
index 0000000..3d5631c
--- /dev/null
+++ b/flumi/Scripts/history_entry.gd
@@ -0,0 +1,10 @@
+extends HBoxContainer
+signal checkbox_toggle
+
+@onready var check_box: CheckBox = $CheckBox
+
+func reset() -> void:
+ check_box.set_pressed_no_signal(false)
+
+func _on_check_box_toggled(toggled_on: bool) -> void:
+ checkbox_toggle.emit(toggled_on)
diff --git a/flumi/Scripts/history_entry.gd.uid b/flumi/Scripts/history_entry.gd.uid
new file mode 100644
index 0000000..b157564
--- /dev/null
+++ b/flumi/Scripts/history_entry.gd.uid
@@ -0,0 +1 @@
+uid://bw5pr4wrf780h
diff --git a/flumi/Scripts/main.gd b/flumi/Scripts/main.gd
index 197de52..8a7d53f 100644
--- a/flumi/Scripts/main.gd
+++ b/flumi/Scripts/main.gd
@@ -4,6 +4,8 @@ extends Control
@onready var website_container: Control = %WebsiteContainer
@onready var website_background: Control = %WebsiteBackground
@onready var tab_container: TabManager = $VBoxContainer/TabContainer
+@onready var search_bar: LineEdit = $VBoxContainer/HBoxContainer/LineEdit
+
const LOADER_CIRCLE = preload("res://Assets/Icons/loader-circle.svg")
const AUTO_SIZING_FLEX_CONTAINER = preload("res://Scripts/AutoSizingFlexContainer.gd")
@@ -53,6 +55,8 @@ func _ready():
DisplayServer.window_set_min_size(MIN_SIZE)
get_viewport().size_changed.connect(_on_viewport_size_changed)
+
+ call_deferred("render")
func _on_viewport_size_changed():
recalculate_percentage_elements(website_container)
@@ -64,7 +68,49 @@ func recalculate_percentage_elements(node: Node):
for child in node.get_children():
recalculate_percentage_elements(child)
+var current_domain = "" # Store current domain for display
+
+func _on_search_submitted(url: String) -> void:
+ print("Search submitted: ", url)
+
+ if GurtProtocol.is_gurt_domain(url):
+ print("Processing as GURT domain")
+
+ var tab = tab_container.tabs[tab_container.active_tab]
+ tab.start_loading()
+
+ var result = await GurtProtocol.handle_gurt_domain(url)
+
+ if result.has("error"):
+ print("GURT domain error: ", result.error)
+ const GLOBE_ICON = preload("res://Assets/Icons/globe.svg")
+ tab.stop_loading()
+ tab.set_icon(GLOBE_ICON)
+
+ var html_bytes = result.html.to_utf8_buffer()
+ render_content(html_bytes)
+
+ if result.has("display_url"):
+ current_domain = result.display_url
+ if not search_bar.has_focus():
+ search_bar.text = current_domain
+ else:
+ print("Non-GURT URL entered: ", url)
+
+func _on_search_focus_entered() -> void:
+ if not current_domain.is_empty():
+ search_bar.text = "gurt://" + current_domain
+
+func _on_search_focus_exited() -> void:
+ if not current_domain.is_empty():
+ search_bar.text = current_domain
+
+
func render() -> void:
+ render_content(Constants.HTML_CONTENT)
+
+func render_content(html_bytes: PackedByteArray) -> void:
+
# Clear existing content
for child in website_container.get_children():
child.queue_free()
@@ -73,8 +119,6 @@ func render() -> void:
FontManager.clear_fonts()
FontManager.set_refresh_callback(refresh_fonts)
- var html_bytes = Constants.HTML_CONTENT
-
var parser: HTMLParser = HTMLParser.new(html_bytes)
var parse_result = parser.parse()
@@ -109,57 +153,59 @@ func render() -> void:
add_child(lua_api)
var i = 0
- while i < body.children.size():
- var element: HTMLParser.HTMLElement = body.children[i]
-
- if should_group_as_inline(element):
- # Create an HBoxContainer for consecutive inline elements
- var inline_elements: Array[HTMLParser.HTMLElement] = []
-
- while i < body.children.size() and should_group_as_inline(body.children[i]):
- inline_elements.append(body.children[i])
- i += 1
-
- var hbox = HBoxContainer.new()
- hbox.add_theme_constant_override("separation", 4)
-
- for inline_element in inline_elements:
- var inline_node = await create_element_node(inline_element, parser)
- if inline_node:
- # Input elements register their own DOM nodes in their init() function
- if inline_element.tag_name not in ["input", "textarea", "select", "button", "audio"]:
- parser.register_dom_node(inline_element, inline_node)
-
- safe_add_child(hbox, inline_node)
- # Handle hyperlinks for all inline elements
- if contains_hyperlink(inline_element) and inline_node is RichTextLabel:
- inline_node.meta_clicked.connect(func(meta): OS.shell_open(str(meta)))
- else:
- print("Failed to create inline element node: ", inline_element.tag_name)
-
- safe_add_child(website_container, hbox)
- continue
-
- var element_node = await create_element_node(element, parser)
- if element_node:
- # Input elements register their own DOM nodes in their init() function
- if element.tag_name not in ["input", "textarea", "select", "button", "audio"]:
- parser.register_dom_node(element, element_node)
+ if body:
+ while i < body.children.size():
+ var element: HTMLParser.HTMLElement = body.children[i]
- # ul/ol handle their own adding
- if element.tag_name != "ul" and element.tag_name != "ol":
- safe_add_child(website_container, element_node)
+ if should_group_as_inline(element):
+ # Create an HBoxContainer for consecutive inline elements
+ var inline_elements: Array[HTMLParser.HTMLElement] = []
- # Handle hyperlinks for all elements
- if contains_hyperlink(element):
- if element_node is RichTextLabel:
- element_node.meta_clicked.connect(func(meta): OS.shell_open(str(meta)))
- elif element_node.has_method("get") and element_node.get("rich_text_label"):
- element_node.rich_text_label.meta_clicked.connect(func(meta): OS.shell_open(str(meta)))
- else:
- print("Couldn't parse unsupported HTML tag \"%s\"" % element.tag_name)
-
- i += 1
+ while i < body.children.size() and should_group_as_inline(body.children[i]):
+ inline_elements.append(body.children[i])
+ i += 1
+
+ var hbox = HBoxContainer.new()
+ hbox.add_theme_constant_override("separation", 4)
+
+ for inline_element in inline_elements:
+ var inline_node = await create_element_node(inline_element, parser)
+ if inline_node:
+
+ # Input elements register their own DOM nodes in their init() function
+ if inline_element.tag_name not in ["input", "textarea", "select", "button", "audio"]:
+ parser.register_dom_node(inline_element, inline_node)
+
+ safe_add_child(hbox, inline_node)
+ # Handle hyperlinks for all inline elements
+ if contains_hyperlink(inline_element) and inline_node is RichTextLabel:
+ inline_node.meta_clicked.connect(func(meta): OS.shell_open(str(meta)))
+ else:
+ print("Failed to create inline element node: ", inline_element.tag_name)
+
+ safe_add_child(website_container, hbox)
+ continue
+
+ var element_node = await create_element_node(element, parser)
+ if element_node:
+
+ # Input elements register their own DOM nodes in their init() function
+ if element.tag_name not in ["input", "textarea", "select", "button", "audio"]:
+ parser.register_dom_node(element, element_node)
+
+ # ul/ol handle their own adding
+ if element.tag_name != "ul" and element.tag_name != "ol":
+ safe_add_child(website_container, element_node)
+
+ if contains_hyperlink(element):
+ if element_node is RichTextLabel:
+ element_node.meta_clicked.connect(func(meta): OS.shell_open(str(meta)))
+ elif element_node.has_method("get") and element_node.get("rich_text_label"):
+ element_node.rich_text_label.meta_clicked.connect(func(meta): OS.shell_open(str(meta)))
+ else:
+ print("Couldn't parse unsupported HTML tag \"%s\"" % element.tag_name)
+
+ i += 1
if scripts.size() > 0 and lua_api:
parser.process_scripts(lua_api, null)
@@ -381,6 +427,9 @@ func create_element_node_internal(element: HTMLParser.HTMLElement, parser: HTMLP
var p_node = P.instantiate()
p_node.init(element, parser)
+ var div_styles = parser.get_element_styles_with_inheritance(element, "", [])
+ StyleManager.apply_styles_to_label(p_node, div_styles, element, parser)
+
var container_for_children = node
if node is PanelContainer and node.get_child_count() > 0:
container_for_children = node.get_child(0) # The VBoxContainer inside
diff --git a/flumi/project.godot b/flumi/project.godot
index 47204fc..041210f 100644
--- a/flumi/project.godot
+++ b/flumi/project.godot
@@ -39,6 +39,10 @@ folder_colors={
"res://Scripts/": "blue"
}
+[gui]
+
+theme/custom_font="uid://fij84uxfqh4h"
+
[input]
NewTab={