multi-tabs
This commit is contained in:
@@ -4,7 +4,6 @@
|
||||
[ext_resource type="Texture2D" uid="uid://gq8g7t4s3ryg" path="res://Assets/Icons/x.svg" id="2_pisds"]
|
||||
[ext_resource type="StyleBox" uid="uid://fd1nftmqox32" path="res://Scenes/Styles/TabHoverDefault.tres" id="3_kjcxk"]
|
||||
[ext_resource type="Texture2D" uid="uid://c7u7a1u1v04bx" path="res://Scenes/Styles/TabGradientDefault.tres" id="3_q3baj"]
|
||||
[ext_resource type="StyleBox" uid="uid://bx3sgro1ageff" path="res://Scenes/Styles/TabDefault.tres" id="4_ib6pj"]
|
||||
[ext_resource type="Texture2D" uid="uid://dglkjumm1q4lo" path="res://Assets/Icons/23x23-empty.svg" id="5_ib6pj"]
|
||||
[ext_resource type="Texture2D" uid="uid://bqpx2lgo0yecb" path="res://Assets/Icons/globe.svg" id="6_ib6pj"]
|
||||
[ext_resource type="StyleBox" uid="uid://dn8exdnk8tjce" path="res://Scenes/Styles/CloseButtonHover.tres" id="6_pisds"]
|
||||
@@ -12,6 +11,10 @@
|
||||
|
||||
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_6bp64"]
|
||||
|
||||
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_1ohlo"]
|
||||
content_margin_left = 8.0
|
||||
content_margin_right = 8.0
|
||||
|
||||
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_344ge"]
|
||||
|
||||
[sub_resource type="Animation" id="Animation_ib6pj"]
|
||||
@@ -103,7 +106,7 @@ unique_name_in_owner = true
|
||||
z_index = -2
|
||||
clip_contents = true
|
||||
layout_mode = 1
|
||||
offset_right = 47.0
|
||||
offset_right = 350.0
|
||||
offset_bottom = 50.0
|
||||
focus_mode = 0
|
||||
mouse_default_cursor_shape = 2
|
||||
@@ -113,7 +116,7 @@ theme_override_constants/icon_max_width = 23
|
||||
theme_override_styles/focus = SubResource("StyleBoxEmpty_6bp64")
|
||||
theme_override_styles/hover = ExtResource("3_kjcxk")
|
||||
theme_override_styles/pressed = ExtResource("3_kjcxk")
|
||||
theme_override_styles/normal = ExtResource("4_ib6pj")
|
||||
theme_override_styles/normal = SubResource("StyleBoxEmpty_1ohlo")
|
||||
action_mode = 0
|
||||
text = "New Tab"
|
||||
icon = ExtResource("5_ib6pj")
|
||||
@@ -124,17 +127,18 @@ text_overrun_behavior = 3
|
||||
unique_name_in_owner = true
|
||||
z_index = 1
|
||||
layout_mode = 0
|
||||
offset_left = -64.0
|
||||
offset_top = 2.0
|
||||
offset_bottom = 50.0
|
||||
offset_left = 252.25
|
||||
offset_top = 0.995
|
||||
offset_right = 316.25
|
||||
offset_bottom = 48.995
|
||||
texture = ExtResource("3_q3baj")
|
||||
|
||||
[node name="Icon" type="TextureRect" parent="Button"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(23, 23)
|
||||
layout_mode = 0
|
||||
offset_left = -23.0
|
||||
offset_left = 10.03
|
||||
offset_top = 13.0
|
||||
offset_right = 33.03
|
||||
offset_bottom = 36.0
|
||||
texture = ExtResource("6_ib6pj")
|
||||
expand_mode = 1
|
||||
@@ -144,12 +148,12 @@ unique_name_in_owner = true
|
||||
z_index = 2
|
||||
custom_minimum_size = Vector2(34, 34)
|
||||
layout_mode = 0
|
||||
offset_left = -34.0
|
||||
offset_left = 315.39
|
||||
offset_top = 12.0
|
||||
offset_right = 349.39
|
||||
offset_bottom = 46.0
|
||||
scale = Vector2(0.75, 0.75)
|
||||
focus_mode = 0
|
||||
mouse_filter = 2
|
||||
mouse_default_cursor_shape = 2
|
||||
theme_override_styles/focus = SubResource("StyleBoxEmpty_344ge")
|
||||
theme_override_styles/hover = ExtResource("6_pisds")
|
||||
@@ -167,3 +171,4 @@ speed_scale = 2.0
|
||||
[connection signal="mouse_entered" from="Button" to="." method="_on_button_mouse_entered"]
|
||||
[connection signal="mouse_exited" from="Button" to="." method="_on_button_mouse_exited"]
|
||||
[connection signal="pressed" from="Button" to="." method="_on_button_pressed"]
|
||||
[connection signal="pressed" from="Button/CloseButton" to="." method="_on_close_button_pressed"]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
[gd_scene load_steps=36 format=3 uid="uid://bytm7bt2s4ak8"]
|
||||
[gd_scene load_steps=35 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"]
|
||||
@@ -85,9 +85,6 @@ expand_margin_left = 40.0
|
||||
LineEdit/styles/focus = SubResource("StyleBoxFlat_ee4r6")
|
||||
LineEdit/styles/normal = SubResource("StyleBoxFlat_cbgmd")
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_white"]
|
||||
bg_color = Color(1, 1, 1, 1)
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_6iyac"]
|
||||
bg_color = Color(0.105882, 0.105882, 0.105882, 1)
|
||||
|
||||
@@ -114,7 +111,7 @@ grow_vertical = 2
|
||||
[node name="Spacer" type="Control" parent="VBoxContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="TabContainer" type="HFlowContainer" parent="VBoxContainer"]
|
||||
[node name="TabContainer" type="HBoxContainer" parent="VBoxContainer"]
|
||||
layout_mode = 2
|
||||
script = ExtResource("2_hptm8")
|
||||
|
||||
@@ -147,6 +144,10 @@ theme_override_styles/normal = SubResource("StyleBoxFlat_6bp64")
|
||||
icon = ExtResource("5_ynf5e")
|
||||
icon_alignment = 1
|
||||
|
||||
[node name="Spacer3" type="Control" parent="VBoxContainer/TabContainer"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
|
||||
[node name="Spacer2" type="Control" parent="VBoxContainer"]
|
||||
custom_minimum_size = Vector2(0, 5)
|
||||
layout_mode = 2
|
||||
@@ -284,19 +285,6 @@ 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
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
offset_top = 122.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
mouse_filter = 2
|
||||
theme_override_styles/panel = SubResource("StyleBoxFlat_white")
|
||||
|
||||
[node name="Panel" type="Panel" parent="."]
|
||||
z_index = -5
|
||||
layout_mode = 1
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
extends Button
|
||||
|
||||
@onready var tab_container: TabManager = $"../../TabContainer"
|
||||
@onready var website_background: Panel = %WebsiteBackground
|
||||
|
||||
func _on_pressed() -> void:
|
||||
%OptionsMenu.show()
|
||||
@@ -15,4 +14,4 @@ func _on_options_menu_id_pressed(id: int) -> void:
|
||||
# TODO: handle incognito
|
||||
OS.create_process(OS.get_executable_path(), ["--incognito"])
|
||||
if id == 4: # history
|
||||
website_background.modulate = Constants.SECONDARY_COLOR
|
||||
modulate = Constants.SECONDARY_COLOR
|
||||
|
||||
@@ -9,6 +9,7 @@ signal tab_closed
|
||||
@onready var close_button: Button = %CloseButton
|
||||
@onready var icon: TextureRect = %Icon
|
||||
@onready var animation: AnimationPlayer = $AnimationPlayer
|
||||
var appear_tween: Tween
|
||||
|
||||
const TAB_GRADIENT: GradientTexture2D = preload("res://Scenes/Styles/TabGradient.tres")
|
||||
const TAB_GRADIENT_DEFAULT: GradientTexture2D = preload("res://Scenes/Styles/TabGradientDefault.tres")
|
||||
@@ -19,10 +20,25 @@ const TAB_DEFAULT: StyleBoxFlat = preload("res://Scenes/Styles/TabDefault.tres")
|
||||
const CLOSE_BUTTON_HOVER: StyleBoxFlat = preload("res://Scenes/Styles/CloseButtonHover.tres")
|
||||
const CLOSE_BUTTON_NORMAL: StyleBoxFlat = preload("res://Scenes/Styles/CloseButtonNormal.tres")
|
||||
|
||||
const CLOSE_BUTTON_HIDE_THRESHOLD := 100
|
||||
const TEXT_HIDE_THRESHOLD := 50
|
||||
const GRADIENT_WIDTH := 64
|
||||
const GRADIENT_OFFSET := 72
|
||||
const CLOSE_BUTTON_OFFSET := 34
|
||||
const ICON_OFFSET := 8
|
||||
const APPEAR_ANIMATION_DURATION := 0.25
|
||||
|
||||
var is_active := false
|
||||
var mouse_over_tab := false
|
||||
var loading_tween: Tween
|
||||
|
||||
var scroll_container: ScrollContainer = null
|
||||
var website_container: VBoxContainer = null
|
||||
var background_panel: PanelContainer = null
|
||||
var lua_apis: Array[LuaAPI] = []
|
||||
var current_url: String = ""
|
||||
var has_content: bool = false
|
||||
|
||||
func _ready():
|
||||
add_to_group("tabs")
|
||||
gradient_texture.texture = gradient_texture.texture.duplicate()
|
||||
@@ -41,6 +57,7 @@ func _process(_delta):
|
||||
|
||||
func set_title(title: String) -> void:
|
||||
button.text = title
|
||||
button.set_meta("original_text", title)
|
||||
|
||||
func set_icon(new_icon: Texture) -> void:
|
||||
icon.texture = new_icon
|
||||
@@ -52,7 +69,6 @@ func update_icon_from_url(icon_url: String) -> void:
|
||||
set_icon(GLOBE_ICON)
|
||||
return
|
||||
|
||||
# Load the icon in the background
|
||||
var icon_resource = await Network.fetch_image(icon_url)
|
||||
|
||||
if is_instance_valid(self) and icon_resource:
|
||||
@@ -98,24 +114,104 @@ func _exit_tree():
|
||||
if loading_tween:
|
||||
loading_tween.kill()
|
||||
loading_tween = null
|
||||
|
||||
for lua_api in lua_apis:
|
||||
if is_instance_valid(lua_api):
|
||||
lua_api.kill_script_execution()
|
||||
lua_api.queue_free()
|
||||
lua_apis.clear()
|
||||
|
||||
if scroll_container and is_instance_valid(scroll_container):
|
||||
if scroll_container.get_parent():
|
||||
scroll_container.get_parent().remove_child(scroll_container)
|
||||
scroll_container.queue_free()
|
||||
|
||||
if background_panel and is_instance_valid(background_panel):
|
||||
if background_panel.get_parent():
|
||||
background_panel.get_parent().remove_child(background_panel)
|
||||
background_panel.queue_free()
|
||||
|
||||
remove_from_group("tabs")
|
||||
|
||||
func _enter_tree() -> void:
|
||||
$AnimationPlayer.play("appear")
|
||||
func init_scene(parent_container: Control) -> void:
|
||||
if not scroll_container:
|
||||
background_panel = PanelContainer.new()
|
||||
background_panel.name = "Tab_Background_" + str(get_instance_id())
|
||||
background_panel.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
background_panel.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
|
||||
var style_box = StyleBoxFlat.new()
|
||||
style_box.bg_color = Color(1, 1, 1, 1) # White background
|
||||
background_panel.add_theme_stylebox_override("panel", style_box)
|
||||
|
||||
scroll_container = ScrollContainer.new()
|
||||
scroll_container.name = "Tab_ScrollContainer_" + str(get_instance_id())
|
||||
scroll_container.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
|
||||
website_container = VBoxContainer.new()
|
||||
website_container.name = "Tab_WebsiteContainer_" + str(get_instance_id())
|
||||
website_container.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
website_container.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
website_container.add_theme_constant_override("separation", 22)
|
||||
|
||||
parent_container.call_deferred("add_child", background_panel)
|
||||
background_panel.call_deferred("add_child", scroll_container)
|
||||
scroll_container.call_deferred("add_child", website_container)
|
||||
|
||||
background_panel.visible = is_active
|
||||
|
||||
func show_content() -> void:
|
||||
if background_panel:
|
||||
background_panel.visible = true
|
||||
|
||||
func play_appear_animation(target_width: float) -> void:
|
||||
var should_hide_close = target_width < CLOSE_BUTTON_HIDE_THRESHOLD
|
||||
var should_hide_text = target_width < TEXT_HIDE_THRESHOLD
|
||||
|
||||
close_button.visible = not should_hide_close
|
||||
button.text = "" if should_hide_text else button.get_meta("original_text", "New Tab")
|
||||
|
||||
var should_show_gradient = not should_hide_text and not should_hide_close
|
||||
gradient_texture.visible = should_show_gradient
|
||||
|
||||
if should_show_gradient:
|
||||
gradient_texture.size.x = GRADIENT_WIDTH
|
||||
gradient_texture.position.x = target_width - GRADIENT_OFFSET
|
||||
|
||||
if not should_hide_close:
|
||||
close_button.position.x = target_width - CLOSE_BUTTON_OFFSET
|
||||
|
||||
icon.position.x = ICON_OFFSET
|
||||
custom_minimum_size.x = 0.0
|
||||
size.x = 0.0
|
||||
button.custom_minimum_size.x = 0.0
|
||||
button.size.x = 0.0
|
||||
|
||||
if appear_tween:
|
||||
appear_tween.kill()
|
||||
|
||||
appear_tween = create_tween()
|
||||
appear_tween.set_ease(Tween.EASE_OUT)
|
||||
appear_tween.set_trans(Tween.TRANS_CUBIC)
|
||||
|
||||
appear_tween.parallel().tween_property(self, "custom_minimum_size:x", target_width, APPEAR_ANIMATION_DURATION)
|
||||
appear_tween.parallel().tween_property(self, "size:x", target_width, APPEAR_ANIMATION_DURATION)
|
||||
appear_tween.parallel().tween_property(button, "custom_minimum_size:x", target_width, APPEAR_ANIMATION_DURATION)
|
||||
appear_tween.parallel().tween_property(button, "size:x", target_width, APPEAR_ANIMATION_DURATION)
|
||||
|
||||
func _on_button_pressed() -> void:
|
||||
# Check if click was on close button area
|
||||
var mouse_pos = get_global_mouse_position()
|
||||
var close_button_rect = Rect2(close_button.global_position, close_button.size * close_button.scale)
|
||||
|
||||
if close_button_rect.has_point(mouse_pos):
|
||||
_on_close_button_pressed()
|
||||
else:
|
||||
# Handle tab button click
|
||||
tab_pressed.emit()
|
||||
tab_pressed.emit()
|
||||
|
||||
func _on_close_button_pressed() -> void:
|
||||
var close_tween = create_tween()
|
||||
close_tween.set_ease(Tween.EASE_IN)
|
||||
close_tween.set_trans(Tween.TRANS_CUBIC)
|
||||
|
||||
close_tween.parallel().tween_property(self, "custom_minimum_size:x", 0.0, 0.15)
|
||||
close_tween.parallel().tween_property(self, "size:x", 0.0, 0.15)
|
||||
close_tween.parallel().tween_property(button, "custom_minimum_size:x", 0.0, 0.15)
|
||||
close_tween.parallel().tween_property(button, "size:x", 0.0, 0.15)
|
||||
|
||||
await close_tween.finished
|
||||
tab_closed.emit()
|
||||
animation.play("appear", -1, -1.0, true)
|
||||
await animation.animation_finished
|
||||
queue_free()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
class_name TabManager
|
||||
extends HFlowContainer
|
||||
extends HBoxContainer
|
||||
|
||||
var tabs: Array[Tab] = []
|
||||
var active_tab := 0
|
||||
@@ -19,13 +19,38 @@ const TAB_GRADIENT_DEFAULT: GradientTexture2D = preload("res://Scenes/Styles/Tab
|
||||
|
||||
@onready var h_box_container: HBoxContainer = $HBoxContainer
|
||||
|
||||
const MIN_TAB_WIDTH = 24 # Minimum width (icon only)
|
||||
const MIN_TAB_WIDTH_WITH_CLOSE = 60 # Minimum width when close button is visible
|
||||
const MAX_TAB_WIDTH = 320 # Max width
|
||||
const CLOSE_BUTTON_HIDE_THRESHOLD = 100 # When to hide close button
|
||||
const TEXT_HIDE_THRESHOLD = 50 # When to hide text
|
||||
const POPUP_BUTTON_WIDTH = 50 # Width of + button
|
||||
const NEW_TAB_BUTTON_WIDTH = 50 # Width of new tab button
|
||||
const OTHER_UI_PADDING = 200 # Space for other UI elements
|
||||
|
||||
func _ready() -> void:
|
||||
tabs.assign(get_tree().get_nodes_in_group("tabs"))
|
||||
|
||||
call_deferred("_initialize_tab_containers")
|
||||
|
||||
set_active_tab(0)
|
||||
|
||||
for i in tabs.size():
|
||||
tabs[i].tab_pressed.connect(_tab_pressed.bind(i))
|
||||
tabs[i].tab_closed.connect(_tab_closed.bind(i))
|
||||
|
||||
get_viewport().size_changed.connect(_on_viewport_resized)
|
||||
|
||||
call_deferred("update_tab_widths")
|
||||
call_deferred("_delayed_update")
|
||||
|
||||
func _initialize_tab_containers() -> void:
|
||||
for tab in tabs:
|
||||
trigger_init_scene(tab)
|
||||
|
||||
func trigger_init_scene(tab: Tab) -> void:
|
||||
var main_vbox = main.get_node("VBoxContainer")
|
||||
tab.init_scene(main_vbox)
|
||||
|
||||
func _tab_pressed(index: int) -> void:
|
||||
set_active_tab(index)
|
||||
@@ -56,34 +81,150 @@ func _tab_closed(index: int) -> void:
|
||||
tabs[i].tab_closed.connect(_tab_closed.bind(i))
|
||||
|
||||
set_active_tab(active_tab)
|
||||
update_tab_widths()
|
||||
|
||||
func _on_viewport_resized() -> void:
|
||||
update_tab_widths()
|
||||
|
||||
func _delayed_update() -> void:
|
||||
update_tab_widths()
|
||||
|
||||
func update_tab_widths() -> void:
|
||||
if tabs.is_empty():
|
||||
return
|
||||
|
||||
var viewport_width = get_viewport().get_visible_rect().size.x
|
||||
var available_width = viewport_width - POPUP_BUTTON_WIDTH - NEW_TAB_BUTTON_WIDTH - OTHER_UI_PADDING
|
||||
|
||||
var tab_width = available_width / float(tabs.size())
|
||||
tab_width = clamp(tab_width, MIN_TAB_WIDTH, MAX_TAB_WIDTH)
|
||||
|
||||
var should_hide_close = tab_width < CLOSE_BUTTON_HIDE_THRESHOLD
|
||||
var should_hide_text = tab_width < TEXT_HIDE_THRESHOLD
|
||||
|
||||
h_box_container.custom_minimum_size.x = 0
|
||||
h_box_container.size.x = 0
|
||||
|
||||
for tab in tabs:
|
||||
if tab.appear_tween and tab.appear_tween.is_valid():
|
||||
continue
|
||||
|
||||
tab.custom_minimum_size.x = tab_width
|
||||
tab.size.x = tab_width
|
||||
|
||||
tab.button.custom_minimum_size.x = tab_width
|
||||
tab.button.size.x = tab_width
|
||||
|
||||
tab.close_button.visible = not should_hide_close
|
||||
tab.button.text = "" if should_hide_text else tab.button.get_meta("original_text", tab.button.text)
|
||||
|
||||
if not tab.button.has_meta("original_text"):
|
||||
tab.button.set_meta("original_text", tab.button.text)
|
||||
|
||||
update_tab_internal_elements(tab, tab_width, should_hide_close, should_hide_text)
|
||||
|
||||
func calculate_visible_tab_count(available_width: float) -> int:
|
||||
var all_tabs_width = calculate_tab_width(available_width, tabs.size())
|
||||
if all_tabs_width >= MIN_TAB_WIDTH:
|
||||
return tabs.size()
|
||||
|
||||
for tab_count in range(tabs.size(), 0, -1):
|
||||
var tab_width = calculate_tab_width(available_width, tab_count)
|
||||
if tab_width >= MIN_TAB_WIDTH:
|
||||
return tab_count
|
||||
|
||||
return max(1, tabs.size())
|
||||
|
||||
func calculate_tab_width(available_width: float, tab_count: int) -> float:
|
||||
if tab_count == 0:
|
||||
return MAX_TAB_WIDTH
|
||||
|
||||
var ideal_width = available_width / tab_count
|
||||
return clamp(ideal_width, MIN_TAB_WIDTH, MAX_TAB_WIDTH)
|
||||
|
||||
func get_hidden_tabs() -> Array[Tab]:
|
||||
var hidden_tabs: Array[Tab] = []
|
||||
for tab in tabs:
|
||||
if not tab.visible:
|
||||
hidden_tabs.append(tab)
|
||||
return hidden_tabs
|
||||
|
||||
func has_hidden_tabs() -> bool:
|
||||
return get_hidden_tabs().size() > 0
|
||||
|
||||
func update_tab_internal_elements(tab: Tab, width: float, hide_close_button: bool = false, hide_text: bool = false) -> void:
|
||||
var should_show_gradient = not hide_text and not hide_close_button
|
||||
tab.gradient_texture.visible = should_show_gradient
|
||||
|
||||
if should_show_gradient:
|
||||
var gradient_start_offset = 72
|
||||
var gradient_width = 64
|
||||
var gradient_start_x = width - gradient_start_offset
|
||||
|
||||
tab.gradient_texture.position.x = gradient_start_x
|
||||
tab.gradient_texture.size.x = gradient_width
|
||||
|
||||
if not hide_close_button:
|
||||
var close_button_x = width - 34
|
||||
tab.close_button.position.x = close_button_x
|
||||
|
||||
func set_active_tab(index: int) -> void:
|
||||
# old tab
|
||||
tabs[active_tab].is_active = false
|
||||
tabs[active_tab].button.add_theme_stylebox_override("normal", TAB_DEFAULT)
|
||||
tabs[active_tab].button.add_theme_stylebox_override("pressed", TAB_DEFAULT)
|
||||
tabs[active_tab].button.add_theme_stylebox_override("hover", TAB_HOVER_DEFAULT)
|
||||
tabs[active_tab].gradient_texture.texture = TAB_GRADIENT_DEFAULT
|
||||
# new tab
|
||||
if index < 0 or index >= tabs.size():
|
||||
return
|
||||
|
||||
if active_tab >= 0 and active_tab < tabs.size():
|
||||
tabs[active_tab].is_active = false
|
||||
tabs[active_tab].button.add_theme_stylebox_override("normal", TAB_DEFAULT)
|
||||
tabs[active_tab].button.add_theme_stylebox_override("pressed", TAB_DEFAULT)
|
||||
tabs[active_tab].button.add_theme_stylebox_override("hover", TAB_HOVER_DEFAULT)
|
||||
tabs[active_tab].gradient_texture.texture = TAB_GRADIENT_DEFAULT
|
||||
if tabs[active_tab].background_panel:
|
||||
tabs[active_tab].background_panel.visible = false
|
||||
|
||||
tabs[index].is_active = true
|
||||
tabs[index].button.add_theme_stylebox_override("normal", TAB_NORMAL)
|
||||
tabs[index].button.add_theme_stylebox_override("pressed", TAB_NORMAL)
|
||||
tabs[index].button.add_theme_stylebox_override("hover", TAB_NORMAL)
|
||||
tabs[index].gradient_texture.texture = TAB_GRADIENT
|
||||
tabs[index].show_content()
|
||||
|
||||
if not tabs[index].website_container:
|
||||
if main:
|
||||
trigger_init_scene(tabs[index])
|
||||
|
||||
active_tab = index
|
||||
|
||||
if main and main.search_bar:
|
||||
if tabs[index].has_content:
|
||||
main.current_domain = tabs[index].current_url
|
||||
var display_text = main.current_domain
|
||||
if display_text.begins_with("gurt://"):
|
||||
display_text = display_text.substr(7)
|
||||
main.search_bar.text = display_text
|
||||
else:
|
||||
main.current_domain = ""
|
||||
main.search_bar.text = ""
|
||||
main.search_bar.grab_focus()
|
||||
|
||||
func create_tab() -> void:
|
||||
var index = tabs.size();
|
||||
var tab = TAB.instantiate()
|
||||
tabs.append(tab)
|
||||
tab.tab_pressed.connect(_tab_pressed.bind(index))
|
||||
tab.tab_closed.connect(_tab_closed.bind(index))
|
||||
var viewport_width = get_viewport().get_visible_rect().size.x
|
||||
var available_width = viewport_width - POPUP_BUTTON_WIDTH - NEW_TAB_BUTTON_WIDTH - OTHER_UI_PADDING
|
||||
var visible_count = calculate_visible_tab_count(available_width)
|
||||
var tab_width = calculate_tab_width(available_width, visible_count)
|
||||
|
||||
h_box_container.add_child(tab)
|
||||
tab.play_appear_animation(tab_width)
|
||||
|
||||
set_active_tab(index)
|
||||
|
||||
await get_tree().process_frame
|
||||
update_tab_widths()
|
||||
|
||||
trigger_init_scene(tab)
|
||||
|
||||
# WARNING: temporary
|
||||
main.render()
|
||||
|
||||
@@ -98,6 +239,9 @@ func _input(_event: InputEvent) -> void:
|
||||
if Input.is_action_just_pressed("PreviousTab"):
|
||||
var prev_tab = (active_tab - 1 + tabs.size()) % tabs.size()
|
||||
set_active_tab(prev_tab - 1)
|
||||
if Input.is_action_just_pressed("FocusSearch"):
|
||||
main.search_bar.grab_focus()
|
||||
main.search_bar.select_all()
|
||||
|
||||
func _on_new_tab_button_pressed() -> void:
|
||||
create_tab()
|
||||
|
||||
@@ -2,7 +2,6 @@ class_name Main
|
||||
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
|
||||
|
||||
@@ -55,6 +54,10 @@ func _ready():
|
||||
|
||||
CertificateManager.initialize()
|
||||
|
||||
var original_scroll = website_container.get_parent()
|
||||
if original_scroll:
|
||||
original_scroll.visible = false
|
||||
|
||||
call_deferred("render")
|
||||
|
||||
var current_domain = "" # Store current domain for display
|
||||
@@ -170,34 +173,49 @@ func render() -> void:
|
||||
render_content(Constants.HTML_CONTENT)
|
||||
|
||||
func render_content(html_bytes: PackedByteArray) -> void:
|
||||
var active_tab = get_active_tab()
|
||||
var target_container: Control
|
||||
|
||||
var existing_lua_apis = []
|
||||
for child in get_children():
|
||||
if child is LuaAPI:
|
||||
existing_lua_apis.append(child)
|
||||
if active_tab and active_tab.website_container:
|
||||
target_container = active_tab.website_container
|
||||
else:
|
||||
target_container = website_container
|
||||
|
||||
if not target_container:
|
||||
print("Error: No container available for rendering")
|
||||
return
|
||||
|
||||
for lua_api in existing_lua_apis:
|
||||
lua_api.kill_script_execution()
|
||||
remove_child(lua_api)
|
||||
lua_api.queue_free()
|
||||
if active_tab:
|
||||
var existing_tab_lua_apis = active_tab.lua_apis
|
||||
for lua_api in existing_tab_lua_apis:
|
||||
if is_instance_valid(lua_api):
|
||||
lua_api.kill_script_execution()
|
||||
remove_child(lua_api)
|
||||
lua_api.queue_free()
|
||||
active_tab.lua_apis.clear()
|
||||
else:
|
||||
var existing_lua_apis = []
|
||||
for child in get_children():
|
||||
if child is LuaAPI:
|
||||
existing_lua_apis.append(child)
|
||||
|
||||
for lua_api in existing_lua_apis:
|
||||
lua_api.kill_script_execution()
|
||||
remove_child(lua_api)
|
||||
lua_api.queue_free()
|
||||
|
||||
# Clear existing content
|
||||
for child in website_container.get_children():
|
||||
if target_container.get_parent() and target_container.get_parent().name == "BodyMarginContainer":
|
||||
var body_margin_container = target_container.get_parent()
|
||||
var scroll_container = body_margin_container.get_parent()
|
||||
if scroll_container:
|
||||
body_margin_container.remove_child(target_container)
|
||||
scroll_container.remove_child(body_margin_container)
|
||||
body_margin_container.queue_free()
|
||||
scroll_container.add_child(target_container)
|
||||
|
||||
for child in target_container.get_children():
|
||||
child.queue_free()
|
||||
|
||||
var current_parent = website_container.get_parent()
|
||||
if current_parent and current_parent.name == "BodyMarginContainer":
|
||||
var original_parent = current_parent.get_parent()
|
||||
var container_index = current_parent.get_index()
|
||||
|
||||
current_parent.remove_child(website_container)
|
||||
|
||||
original_parent.remove_child(current_parent)
|
||||
current_parent.queue_free()
|
||||
|
||||
original_parent.add_child(website_container)
|
||||
original_parent.move_child(website_container, container_index)
|
||||
|
||||
font_dependent_elements.clear()
|
||||
FontManager.clear_fonts()
|
||||
FontManager.set_refresh_callback(refresh_fonts)
|
||||
@@ -217,7 +235,7 @@ func render_content(html_bytes: PackedByteArray) -> void:
|
||||
if parse_result.errors.size() > 0:
|
||||
print("Parse errors: " + str(parse_result.errors))
|
||||
|
||||
var tab = tab_container.tabs[tab_container.active_tab]
|
||||
var tab = active_tab
|
||||
|
||||
var title = parser.get_title()
|
||||
tab.set_title(title)
|
||||
@@ -228,15 +246,19 @@ func render_content(html_bytes: PackedByteArray) -> void:
|
||||
var body = parser.find_first("body")
|
||||
|
||||
if body:
|
||||
StyleManager.apply_body_styles(body, parser, website_container, website_background)
|
||||
var background_panel = active_tab.background_panel
|
||||
|
||||
parser.register_dom_node(body, website_container)
|
||||
StyleManager.apply_body_styles(body, parser, target_container, background_panel)
|
||||
|
||||
parser.register_dom_node(body, target_container)
|
||||
|
||||
var scripts = parser.find_all("script")
|
||||
var lua_api = null
|
||||
if scripts.size() > 0:
|
||||
lua_api = LuaAPI.new()
|
||||
add_child(lua_api)
|
||||
if active_tab:
|
||||
active_tab.lua_apis.append(lua_api)
|
||||
|
||||
var i = 0
|
||||
if body:
|
||||
@@ -255,7 +277,7 @@ func render_content(html_bytes: PackedByteArray) -> void:
|
||||
hbox.add_theme_constant_override("separation", 4)
|
||||
|
||||
for inline_element in inline_elements:
|
||||
var inline_node = await create_element_node(inline_element, parser)
|
||||
var inline_node = await create_element_node(inline_element, parser, target_container)
|
||||
if inline_node:
|
||||
|
||||
# Input elements register their own DOM nodes in their init() function
|
||||
@@ -269,10 +291,10 @@ func render_content(html_bytes: PackedByteArray) -> void:
|
||||
else:
|
||||
print("Failed to create inline element node: ", inline_element.tag_name)
|
||||
|
||||
safe_add_child(website_container, hbox)
|
||||
safe_add_child(target_container, hbox)
|
||||
continue
|
||||
|
||||
var element_node = await create_element_node(element, parser)
|
||||
var element_node = await create_element_node(element, parser, target_container)
|
||||
if element_node:
|
||||
|
||||
# Input elements register their own DOM nodes in their init() function
|
||||
@@ -281,7 +303,7 @@ func render_content(html_bytes: PackedByteArray) -> void:
|
||||
|
||||
# ul/ol handle their own adding
|
||||
if element.tag_name != "ul" and element.tag_name != "ol":
|
||||
safe_add_child(website_container, element_node)
|
||||
safe_add_child(target_container, element_node)
|
||||
|
||||
if contains_hyperlink(element):
|
||||
if element_node is RichTextLabel:
|
||||
@@ -297,6 +319,9 @@ func render_content(html_bytes: PackedByteArray) -> void:
|
||||
parser.process_scripts(lua_api, null)
|
||||
if parse_result.external_scripts and not parse_result.external_scripts.is_empty():
|
||||
await parser.process_external_scripts(lua_api, null, current_domain)
|
||||
|
||||
active_tab.current_url = current_domain
|
||||
active_tab.has_content = true
|
||||
|
||||
static func safe_add_child(parent: Node, child: Node) -> void:
|
||||
if child.get_parent():
|
||||
@@ -321,7 +346,7 @@ func is_text_only_element(element: HTMLParser.HTMLElement) -> bool:
|
||||
|
||||
return false
|
||||
|
||||
func create_element_node(element: HTMLParser.HTMLElement, parser: HTMLParser) -> Control:
|
||||
func create_element_node(element: HTMLParser.HTMLElement, parser: HTMLParser, container: Control = null) -> Control:
|
||||
var styles = parser.get_element_styles_with_inheritance(element, "", [])
|
||||
var hover_styles = parser.get_element_styles_with_inheritance(element, "hover", [])
|
||||
var is_flex_container = styles.has("display") and ("flex" in styles["display"])
|
||||
@@ -333,7 +358,7 @@ func create_element_node(element: HTMLParser.HTMLElement, parser: HTMLParser) ->
|
||||
# If this is an inline element AND not a flex or grid container, do NOT recursively add child nodes for its children.
|
||||
# Only create a node for the outermost inline group; nested inline tags are handled by BBCode.
|
||||
if element.is_inline_element() and not is_flex_container and not is_grid_container:
|
||||
final_node = await create_element_node_internal(element, parser)
|
||||
final_node = await create_element_node_internal(element, parser, container if container else get_active_website_container())
|
||||
if not final_node:
|
||||
return null
|
||||
final_node = StyleManager.apply_element_styles(final_node, element, parser)
|
||||
@@ -384,28 +409,29 @@ func create_element_node(element: HTMLParser.HTMLElement, parser: HTMLParser) ->
|
||||
if element.tag_name == "ul" or element.tag_name == "ol":
|
||||
final_node.flex_direction = FlexContainer.FlexDirection.Column
|
||||
|
||||
website_container.add_child(final_node)
|
||||
var active_container = container if container else get_active_website_container()
|
||||
active_container.add_child(final_node)
|
||||
|
||||
var temp_list = UL.instantiate() if element.tag_name == "ul" else OL.instantiate()
|
||||
website_container.add_child(temp_list)
|
||||
active_container.add_child(temp_list)
|
||||
await temp_list.init(element, parser)
|
||||
|
||||
for child in temp_list.get_children():
|
||||
temp_list.remove_child(child)
|
||||
container_for_children.add_child(child)
|
||||
|
||||
website_container.remove_child(temp_list)
|
||||
active_container.remove_child(temp_list)
|
||||
temp_list.queue_free()
|
||||
# If the element itself has text (like <span style="flex">TEXT</span>)
|
||||
elif not element.text_content.is_empty():
|
||||
var new_node = await create_element_node_internal(element, parser)
|
||||
var new_node = await create_element_node_internal(element, parser, container if container else get_active_website_container())
|
||||
if new_node:
|
||||
container_for_children.add_child(new_node)
|
||||
# For flex divs, we're done - no additional node creation needed
|
||||
elif element.tag_name == "div":
|
||||
pass
|
||||
else:
|
||||
final_node = await create_element_node_internal(element, parser)
|
||||
final_node = await create_element_node_internal(element, parser, container if container else get_active_website_container())
|
||||
if not final_node:
|
||||
return null # Unsupported tag
|
||||
|
||||
@@ -456,7 +482,7 @@ func create_element_node(element: HTMLParser.HTMLElement, parser: HTMLParser) ->
|
||||
# Only add child nodes if the child is NOT an inline element
|
||||
# UNLESS the parent is a flex or grid container (inline elements become flex/grid items)
|
||||
if not child_element.is_inline_element() or is_flex_container or is_grid_container:
|
||||
var child_node = await create_element_node(child_element, parser)
|
||||
var child_node = await create_element_node(child_element, parser, container)
|
||||
if child_node and is_instance_valid(container_for_children):
|
||||
# Input elements register their own DOM nodes in their init() function
|
||||
if child_element.tag_name not in ["input", "textarea", "select", "button", "audio"]:
|
||||
@@ -471,7 +497,7 @@ func create_element_node(element: HTMLParser.HTMLElement, parser: HTMLParser) ->
|
||||
|
||||
return final_node
|
||||
|
||||
func create_element_node_internal(element: HTMLParser.HTMLElement, parser: HTMLParser) -> Control:
|
||||
func create_element_node_internal(element: HTMLParser.HTMLElement, parser: HTMLParser, container: Control = null) -> Control:
|
||||
var node: Control = null
|
||||
|
||||
match element.tag_name:
|
||||
@@ -512,7 +538,7 @@ func create_element_node_internal(element: HTMLParser.HTMLElement, parser: HTMLP
|
||||
|
||||
# Manually process children for non-flex forms
|
||||
for child_element in element.children:
|
||||
var child_node = await create_element_node(child_element, parser)
|
||||
var child_node = await create_element_node(child_element, parser, container)
|
||||
if child_node:
|
||||
safe_add_child(node, child_node)
|
||||
"input":
|
||||
@@ -526,12 +552,14 @@ func create_element_node_internal(element: HTMLParser.HTMLElement, parser: HTMLP
|
||||
node.init(element, parser)
|
||||
"ul":
|
||||
node = UL.instantiate()
|
||||
website_container.add_child(node)
|
||||
var ul_container = container if container else website_container
|
||||
ul_container.add_child(node)
|
||||
await node.init(element, parser)
|
||||
return node
|
||||
"ol":
|
||||
node = OL.instantiate()
|
||||
website_container.add_child(node)
|
||||
var ol_container = container if container else website_container
|
||||
ol_container.add_child(node)
|
||||
await node.init(element, parser)
|
||||
return node
|
||||
"li":
|
||||
@@ -612,3 +640,21 @@ func reload_current_page() -> void:
|
||||
func navigate_to_url(url: String) -> void:
|
||||
var resolved_url = resolve_url(url)
|
||||
_on_search_submitted(resolved_url)
|
||||
|
||||
func update_search_bar_from_current_domain() -> void:
|
||||
if not search_bar.has_focus() and not current_domain.is_empty():
|
||||
var display_text = current_domain
|
||||
if display_text.begins_with("gurt://"):
|
||||
display_text = display_text.substr(7)
|
||||
search_bar.text = display_text
|
||||
|
||||
func get_active_tab() -> Tab:
|
||||
if tab_container.active_tab >= 0 and tab_container.active_tab < tab_container.tabs.size():
|
||||
return tab_container.tabs[tab_container.active_tab]
|
||||
return null
|
||||
|
||||
func get_active_website_container() -> Control:
|
||||
var active_tab = get_active_tab()
|
||||
if active_tab:
|
||||
return active_tab.website_container
|
||||
return website_container # fallback to original container
|
||||
|
||||
@@ -67,3 +67,8 @@ PreviousTab={
|
||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":true,"ctrl_pressed":true,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194306,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
FocusSearch={
|
||||
"deadzone": 0.2,
|
||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":true,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":76,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user