aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authordec05eba <dec05eba@protonmail.com>2021-09-18 17:03:11 +0200
committerdec05eba <dec05eba@protonmail.com>2021-09-18 17:03:11 +0200
commit63774155016ad581dcf418c94cd2ec84fcf86445 (patch)
treece4a9939d2555dce80792a06d1361b9f5d8b8ec0 /src
parent86d23983d8c9cd75af9c40f038f6b9a1f0b5fbb5 (diff)
Render selected item background as a rectangle on top instead of banding
Limit selected item background position to body content position and size
Diffstat (limited to 'src')
-rw-r--r--src/Body.cpp306
-rw-r--r--src/RoundedRectangle.cpp33
2 files changed, 201 insertions, 138 deletions
diff --git a/src/Body.cpp b/src/Body.cpp
index 1b300c5..5509938 100644
--- a/src/Body.cpp
+++ b/src/Body.cpp
@@ -609,21 +609,17 @@ namespace QuickMedia {
const float scissor_y = pos.y;
pos.y = 0.0f;
- if(!rendering_card_view)
+ if(!rendering_card_view) {
pos.x += body_spacing[body_theme].body_padding_horizontal;
+ size.x = std::max(0.0f, size.x - body_spacing[body_theme].body_padding_horizontal * 2.0f);
+ }
if(attach_side == AttachSide::TOP)
pos.y += body_spacing[body_theme].body_padding_vertical;
- if(!rendering_card_view)
- size.x = std::max(0.0f, size.x - body_spacing[body_theme].body_padding_horizontal * 2.0f);
-
float frame_time = frame_timer.restart().asSeconds();
if(frame_time > 1.0f)
frame_time = 1.0f;
- // TODO: Remove the need for this. This is needed because fps is changed to 20 or 2 when idle. That idle handler should be removed
- //if(frame_time > 0.01666f)
- // frame_time = 0.01666f;
if(selected_item >= (int)items.size())
selected_item = (int)items.size() - 1;
@@ -637,7 +633,6 @@ namespace QuickMedia {
elapsed_time_sec = draw_timer.getElapsedTime().asSeconds();
- const int prev_num_visible_items = num_visible_items;
const bool prev_items_cut_off = top_cut_off || bottom_cut_off;
const int prev_first_visible_item = first_visible_item;
const int prev_last_visible_item = last_visible_item;
@@ -781,7 +776,7 @@ namespace QuickMedia {
if(rendering_card_view) {
draw_card_view(window, pos, size, window_size, scissor_y);
} else {
- body_total_height = draw_list_view(window, pos, size, prev_num_visible_items, content_progress);
+ body_total_height = draw_list_view(window, pos, size, content_progress);
}
window.setView(prev_view);
@@ -1002,16 +997,17 @@ namespace QuickMedia {
}
}
- float Body::draw_list_view(sf::RenderWindow &window, sf::Vector2f pos, sf::Vector2f size, const int prev_num_visible_items, const Json::Value &content_progress) {
- const int num_items = items.size();
+ static float clamp(float value, float min, float max) {
+ return std::min(max, std::max(min, value));
+ }
- if(prev_num_visible_items > 0 && render_selected_item_bg && body_theme == BODY_THEME_MINIMAL) {
- item_background.set_position(sf::Vector2f(pos.x, item_background_prev_pos.y));
- item_background.set_size(item_background_prev_size);
- item_background.set_color(get_theme().selected_color);
- item_background.set_band(sf::Vector2f(0.0f, 0.0f), sf::Vector2f(0.0f, 0.0f));
- item_background.draw(window);
- }
+ static sf::Vector2f round(sf::Vector2f vec) {
+ return { std::floor(vec.x), std::floor(vec.y) };
+ }
+
+ float Body::draw_list_view(sf::RenderWindow &window, sf::Vector2f pos, sf::Vector2f size, const Json::Value &content_progress) {
+ const int num_items = items.size();
+ const float pos_y_before_scroll = pos.y;
int index = -1;
if(attach_side == AttachSide::TOP) {
@@ -1083,22 +1079,29 @@ namespace QuickMedia {
get_item_height(item.get(), size.x, true, true, merge_with_previous, index);
if(attach_side == AttachSide::BOTTOM)
pos.y -= (item->loaded_height + body_spacing[body_theme].spacing_y);
- //page_scroll += add_height;
-
- //const float top_y_clamped = clamp(pos.y, 0.0f, size.y);
- //const float bottom_y_clamped = std::min(pos.y + item->current_loaded_height, size.y);
-
- //float offset_y = 0.0f;
- //if(pos.y < 0.0f)
- // offset_y = pos.y;
- //else if(pos.y > size.y)
- // offset_y = size.y - pos.y;
+
+ RenderItem render_item;
+ render_item.body_item = item;
+ render_item.pos = pos;
+ render_item.size = size;
+ render_item.item_height = item->loaded_height;
+ render_item.item_index = index;
+ render_item.merge_with_previous = merge_with_previous;
+ render_items.push_back(std::move(render_item));
- //sf::View new_view(sf::FloatRect(0.0f, 0.0f, window_size.x, scissor_y + bottom_y_clamped));
- //new_view.setViewport(sf::FloatRect(0.0f, (scissor_y + top_y_clamped) / (float)window_size.y, 1.0f, (scissor_y + bottom_y_clamped) / (float)window_size.y));
- //window.setView(new_view);
+ {
+ sf::Vector2f item_pos;
+ item_pos.x = std::floor(pos.x);
+ item_pos.y = std::floor(pos.y);
+
+ if(body_theme == BODY_THEME_MODERN_SPACIOUS) {
+ item_background.set_position(item_pos);
+ item_background.set_size(sf::Vector2f(size.x, item->loaded_height));
+ item_background.set_color(get_theme().shade_color);
+ item_background.draw(window);
+ }
+ }
- draw_item(window, item, pos/*sf::Vector2f(pos.x, offset_y)*/, size, item->loaded_height, index, content_progress, true, merge_with_previous);
handle_item_render(pos, size.x, item->loaded_height, index);
++num_visible_items;
@@ -1151,6 +1154,33 @@ namespace QuickMedia {
}
}
+ for(RenderItem &render_item : render_items) {
+ if(render_item.item_index == selected_item) {
+ float offset_y_spacing = -body_spacing[body_theme].spacing_y;
+ if(attach_side == AttachSide::BOTTOM)
+ offset_y_spacing = body_spacing[body_theme].spacing_y;
+
+ if(selected_item_fits_in_body) {
+ item_background.set_position(round(
+ sf::Vector2f(
+ item_background_prev_pos.x,
+ clamp(item_background_prev_pos.y, pos_y_before_scroll, pos_y_before_scroll + size.y - item_background_prev_size.y - body_spacing[body_theme].body_padding_vertical + offset_y_spacing))));
+ } else {
+ item_background.set_position(round(item_background_prev_pos));
+ }
+
+ item_background.set_size(item_background_prev_size);
+ item_background.set_color(get_theme().selected_color);
+ item_background.draw(window);
+ break;
+ }
+ }
+
+ for(RenderItem &render_item : render_items) {
+ draw_item(window, render_item.body_item, render_item.pos, render_item.size, render_item.item_height, render_item.item_index, content_progress, true, render_item.merge_with_previous);
+ }
+
+ render_items.clear();
return pos.y - pos_y_start;
}
@@ -1230,95 +1260,25 @@ namespace QuickMedia {
item_height = std::min(card_height, item_height + (item_thumbnail ? card_image_text_padding : 0) + card_padding_y * 2 + 5);
row_max_height = std::max(row_max_height, item_height);
- handle_item_render(pos + pos_offset, card_width, item_height, item_index);
+ RenderItem render_item;
+ render_item.body_item = item;
+ render_item.pos = pos;
+ render_item.pos_offset = pos_offset;
+ render_item.item_height = item_height;
+ render_item.item_index = item_index;
+ render_item.item_thumbnail = item_thumbnail;
+ render_items.push_back(std::move(render_item));
+
sf::View new_view(sf::FloatRect(0.0f, 0.0f, window_size.x, size.y));
new_view.setViewport(sf::FloatRect(0.0f, scissor_y / (float)window_size.y, 1.0f, size.y / (float)window_size.y));
window.setView(new_view);
- item_background.set_position(pos + pos_offset);
- item_background.set_size(sf::Vector2f(card_width, item_height));
+ item_background.set_position(round(pos + pos_offset));
+ item_background.set_size(round(sf::Vector2f(card_width, item_height)));
item_background.set_color(get_theme().card_item_background_color);
- item_background.set_band(item_background_prev_pos - (pos + pos_offset), item_background_prev_size);
- item_background.set_band_color(get_theme().selected_color);
item_background.draw(window);
- {
- float image_height = 0.0f;
- if(item_thumbnail && item_thumbnail->loading_state == LoadingState::APPLIED_TO_TEXTURE && item_thumbnail->texture.getNativeHandle() != 0) {
- image.setTexture(item_thumbnail->texture, true);
- auto image_size = image.getTexture()->getSize();
- sf::Vector2f image_size_f(image_size.x, image_size.y);
- sf::Vector2f content_size = to_vec2f(thumbnail_size);
- auto new_image_size = clamp_to_size(image_size_f, content_size);
- auto image_scale = get_ratio(image_size_f, new_image_size);
- image.setScale(image_scale);
- image.setPosition(pos + pos_offset + sf::Vector2f(card_padding_x, card_padding_y) + sf::Vector2f(card_max_image_size.x * 0.5f, 0.0f) - sf::Vector2f(new_image_size.x * 0.5f, 0.0f));
- image_height = new_image_size.y;
- if(thumbnail_mask_shader && item->thumbnail_mask_type == ThumbnailMaskType::CIRCLE) {
- thumbnail_mask_shader->setUniform("resolution", new_image_size);
- window.draw(image, thumbnail_mask_shader);
- } else if(rounded_rectangle_mask_shader) {
- rounded_rectangle_mask_shader->setUniform("radius", 10.0f);
- rounded_rectangle_mask_shader->setUniform("resolution", new_image_size);
- window.draw(image, rounded_rectangle_mask_shader);
- } else {
- window.draw(image);
- }
- } else if(!item->thumbnail_url.empty()) {
- sf::Vector2f content_size = to_vec2f(thumbnail_size);
- sf::Vector2f loading_icon_size(loading_icon.getTexture()->getSize().x, loading_icon.getTexture()->getSize().y);
- auto new_loading_icon_size = clamp_to_size(loading_icon_size, content_size);
- loading_icon.setPosition(pos + pos_offset + sf::Vector2f(card_padding_x, card_padding_y) + sf::Vector2f(card_max_image_size.x, content_size.y) * 0.5f);
- loading_icon.setScale(get_ratio(loading_icon_size, new_loading_icon_size));
- loading_icon.setRotation(elapsed_time_sec * 400.0);
- window.draw(loading_icon);
- image_height = content_size.y;
- }
-
- const float text_padding = item_thumbnail ? card_image_text_padding : 0.0f;
-
- sf::Vector2f text_pos = sf::Vector2f(pos.x, scissor_y + body_spacing[body_theme].body_padding_vertical) + pos_offset + sf::Vector2f(card_padding_x, card_padding_y) + sf::Vector2f(0.0f, image_height + text_padding);
- const float text_height = (item_height - card_padding_y * 2.0f) - image_height - text_padding;
-
- const float underflow_text = text_pos.y - scissor_y;
- const float underflow_height = underflow_text < 0.0f ? std::max(0.0f, text_height + underflow_text) : text_height;
- sf::View new_view(sf::FloatRect(0.0f, 0.0f, window_size.x, underflow_height));
- new_view.setViewport(sf::FloatRect(0.0f, std::max(text_pos.y, scissor_y) / (float)window_size.y, 1.0f, underflow_height / (float)window_size.y));
- window.setView(new_view);
-
- text_pos.y = std::min(0.0f, underflow_text);
- float text_offset_y = 0.0f;
- if(item->author_text) {
- item->author_text->setPosition(text_pos);
- item->author_text->draw(window);
- text_offset_y += item->author_text->getHeight();
- }
-
- if(item->title_text) {
- item->title_text->setPosition(text_pos + sf::Vector2f(0.0f, text_offset_y));
- item->title_text->draw(window);
- text_offset_y += item->title_text->getHeight();
- }
-
- if(item->description_text) {
- item->description_text->setPosition(text_pos + sf::Vector2f(0.0f, text_offset_y));
- item->description_text->draw(window);
- text_offset_y += item->description_text->getHeight();
- }
-
- const float gradient_height = 5.0f;
- if(text_offset_y >= text_height - gradient_height && std::abs(item_height - card_height) < 1) {
- const sf::Vector2f card_bottom(text_pos.x, text_height);
- const sf::Color color = item_index == selected_item ? get_theme().selected_color : get_theme().card_item_background_color;
-
- sf::Vertex gradient_points[4];
- gradient_points[0] = sf::Vertex(card_bottom + sf::Vector2f(0.0f, -gradient_height), sf::Color(color.r, color.g, color.b, 0));
- gradient_points[1] = sf::Vertex(card_bottom + sf::Vector2f(card_max_image_size.x, -gradient_height), sf::Color(color.r, color.g, color.b, 0));
- gradient_points[2] = sf::Vertex(card_bottom + sf::Vector2f(card_max_image_size.x, 0.0f), color);
- gradient_points[3] = sf::Vertex(card_bottom + sf::Vector2f(0.0f, 0.0f), color);
- window.draw(gradient_points, 4, sf::Quads);
- }
- }
+ handle_item_render(pos + pos_offset, card_width, item_height, item_index);
++num_visible_items;
if(first_visible_item == -1 || item_index < first_visible_item)
@@ -1359,6 +1319,29 @@ namespace QuickMedia {
++item_index;
}
+ sf::View new_view(sf::FloatRect(0.0f, 0.0f, window_size.x, size.y));
+ new_view.setViewport(sf::FloatRect(0.0f, scissor_y / (float)window_size.y, 1.0f, size.y / (float)window_size.y));
+ window.setView(new_view);
+
+ for(RenderItem &render_item : render_items) {
+ if(render_item.item_index == selected_item) {
+ item_background.set_position(round(
+ sf::Vector2f(
+ item_background_prev_pos.x,
+ clamp(item_background_prev_pos.y, pos.y, pos.y + size.y - item_background_prev_size.y - body_spacing[body_theme].body_padding_vertical - body_spacing[body_theme].spacing_y))));
+ item_background.set_size(round(item_background_prev_size));
+ item_background.set_color(get_theme().selected_color);
+ item_background.draw(window);
+ break;
+ }
+ }
+
+ for(RenderItem &render_item : render_items) {
+ draw_card_item(window, render_item.body_item, render_item.pos, render_item.pos_offset, size, sf::Vector2f(window_size.x, window_size.y), render_item.item_height, scissor_y, render_item.item_index, render_item.item_thumbnail.get());
+ }
+
+ render_items.clear();
+
if(row_has_selected_item)
item_background_target_size = sf::Vector2f(card_width, row_max_height);
}
@@ -1378,15 +1361,6 @@ namespace QuickMedia {
item_pos.x = std::floor(pos.x);
item_pos.y = std::floor(pos.y);
- if(item_index != -1 && body_theme == BODY_THEME_MODERN_SPACIOUS) {
- item_background.set_size(sf::Vector2f(size.x, item_height));
- item_background.set_position(item_pos);
- item_background.set_color(get_theme().shade_color);
- item_background.set_band(item_background_prev_pos - pos, item_background_prev_size);
- item_background.set_band_color(get_theme().selected_color);
- item_background.draw(window);
- }
-
const float padding_y = item_thumbnail ? body_spacing[body_theme].padding_y : body_spacing[body_theme].padding_y_text_only;
float text_offset_x = body_spacing[body_theme].padding_x;
@@ -1550,6 +1524,92 @@ namespace QuickMedia {
}
}
+ void Body::draw_card_item(sf::RenderWindow &window, std::shared_ptr<BodyItem> &item, const sf::Vector2f &pos, const sf::Vector2f &pos_offset, const sf::Vector2f &body_size, const sf::Vector2f &window_size, float item_height, float scissor_y, int item_index, ThumbnailData *item_thumbnail) {
+ const sf::Vector2i thumbnail_size = get_item_thumbnail_size(item.get());
+
+ sf::View new_view(sf::FloatRect(0.0f, 0.0f, window_size.x, body_size.y));
+ new_view.setViewport(sf::FloatRect(0.0f, scissor_y / (float)window_size.y, 1.0f, body_size.y / (float)window_size.y));
+ window.setView(new_view);
+
+ {
+ float image_height = 0.0f;
+ if(item_thumbnail && item_thumbnail->loading_state == LoadingState::APPLIED_TO_TEXTURE && item_thumbnail->texture.getNativeHandle() != 0) {
+ image.setTexture(item_thumbnail->texture, true);
+ auto image_size = image.getTexture()->getSize();
+ sf::Vector2f image_size_f(image_size.x, image_size.y);
+ sf::Vector2f content_size = to_vec2f(thumbnail_size);
+ auto new_image_size = clamp_to_size(image_size_f, content_size);
+ auto image_scale = get_ratio(image_size_f, new_image_size);
+ image.setScale(image_scale);
+ image.setPosition(pos + pos_offset + sf::Vector2f(card_padding_x, card_padding_y) + sf::Vector2f(card_max_image_size.x * 0.5f, 0.0f) - sf::Vector2f(new_image_size.x * 0.5f, 0.0f));
+ image_height = new_image_size.y;
+ if(thumbnail_mask_shader && item->thumbnail_mask_type == ThumbnailMaskType::CIRCLE) {
+ thumbnail_mask_shader->setUniform("resolution", new_image_size);
+ window.draw(image, thumbnail_mask_shader);
+ } else if(rounded_rectangle_mask_shader) {
+ rounded_rectangle_mask_shader->setUniform("radius", 10.0f);
+ rounded_rectangle_mask_shader->setUniform("resolution", new_image_size);
+ window.draw(image, rounded_rectangle_mask_shader);
+ } else {
+ window.draw(image);
+ }
+ } else if(!item->thumbnail_url.empty()) {
+ sf::Vector2f content_size = to_vec2f(thumbnail_size);
+ sf::Vector2f loading_icon_size(loading_icon.getTexture()->getSize().x, loading_icon.getTexture()->getSize().y);
+ auto new_loading_icon_size = clamp_to_size(loading_icon_size, content_size);
+ loading_icon.setPosition(pos + pos_offset + sf::Vector2f(card_padding_x, card_padding_y) + sf::Vector2f(card_max_image_size.x, content_size.y) * 0.5f);
+ loading_icon.setScale(get_ratio(loading_icon_size, new_loading_icon_size));
+ loading_icon.setRotation(elapsed_time_sec * 400.0);
+ window.draw(loading_icon);
+ image_height = content_size.y;
+ }
+
+ const float text_padding = item_thumbnail ? card_image_text_padding : 0.0f;
+
+ sf::Vector2f text_pos = sf::Vector2f(pos.x, scissor_y + body_spacing[body_theme].body_padding_vertical) + pos_offset + sf::Vector2f(card_padding_x, card_padding_y) + sf::Vector2f(0.0f, image_height + text_padding);
+ const float text_height = (item_height - card_padding_y * 2.0f) - image_height - text_padding;
+
+ const float underflow_text = text_pos.y - scissor_y;
+ const float underflow_height = underflow_text < 0.0f ? std::max(0.0f, text_height + underflow_text) : text_height;
+ sf::View new_view(sf::FloatRect(0.0f, 0.0f, window_size.x, underflow_height));
+ new_view.setViewport(sf::FloatRect(0.0f, std::max(text_pos.y, scissor_y) / (float)window_size.y, 1.0f, underflow_height / (float)window_size.y));
+ window.setView(new_view);
+
+ text_pos.y = std::min(0.0f, underflow_text);
+ float text_offset_y = 0.0f;
+ if(item->author_text) {
+ item->author_text->setPosition(text_pos);
+ item->author_text->draw(window);
+ text_offset_y += item->author_text->getHeight();
+ }
+
+ if(item->title_text) {
+ item->title_text->setPosition(text_pos + sf::Vector2f(0.0f, text_offset_y));
+ item->title_text->draw(window);
+ text_offset_y += item->title_text->getHeight();
+ }
+
+ if(item->description_text) {
+ item->description_text->setPosition(text_pos + sf::Vector2f(0.0f, text_offset_y));
+ item->description_text->draw(window);
+ text_offset_y += item->description_text->getHeight();
+ }
+
+ const float gradient_height = 5.0f;
+ if(text_offset_y >= text_height - gradient_height && std::abs(item_height - card_height) < 1) {
+ const sf::Vector2f card_bottom(text_pos.x, text_height);
+ const sf::Color color = item_index == selected_item ? get_theme().selected_color : get_theme().card_item_background_color;
+
+ sf::Vertex gradient_points[4];
+ gradient_points[0] = sf::Vertex(card_bottom + sf::Vector2f(0.0f, -gradient_height), sf::Color(color.r, color.g, color.b, 0));
+ gradient_points[1] = sf::Vertex(card_bottom + sf::Vector2f(card_max_image_size.x, -gradient_height), sf::Color(color.r, color.g, color.b, 0));
+ gradient_points[2] = sf::Vertex(card_bottom + sf::Vector2f(card_max_image_size.x, 0.0f), color);
+ gradient_points[3] = sf::Vertex(card_bottom + sf::Vector2f(0.0f, 0.0f), color);
+ window.draw(gradient_points, 4, sf::Quads);
+ }
+ }
+ }
+
float Body::get_item_height(BodyItem *item, float width, bool load_texture, bool include_embedded_item, bool merge_with_previous, int item_index) {
const bool rendering_card_view = card_view && card_view_enabled;
sf::Vector2i content_size = get_item_thumbnail_size(item);
diff --git a/src/RoundedRectangle.cpp b/src/RoundedRectangle.cpp
index 4776618..fc56823 100644
--- a/src/RoundedRectangle.cpp
+++ b/src/RoundedRectangle.cpp
@@ -1,13 +1,17 @@
#include "../include/RoundedRectangle.hpp"
#include <SFML/Graphics/Shader.hpp>
#include <SFML/Graphics/RenderTarget.hpp>
+#include <atomic>
#include <assert.h>
namespace QuickMedia {
static const float shadow_radius = 20.0f; // Has to match the shadow offset in rounded_rectangle.glsl
+ static std::atomic<float> cached_radius = 0.0f;
+ static std::atomic<float> cached_resolution_x = 0.0f;
+ static std::atomic<float> cached_resolution_y = 0.0f;
RoundedRectangle::RoundedRectangle(sf::Vector2f size, float radius, sf::Color color, sf::Shader *rounded_rectangle_shader) :
- radius(radius), pos(0.0f, 0.0f), size(size), rounded_rectangle_shader(rounded_rectangle_shader), band_color(sf::Color::Transparent)
+ radius(radius), pos(0.0f, 0.0f), size(size), rounded_rectangle_shader(rounded_rectangle_shader)
{
assert(rounded_rectangle_shader);
set_color(color);
@@ -44,22 +48,21 @@ namespace QuickMedia {
return size;
}
- void RoundedRectangle::set_band(sf::Vector2f pos, sf::Vector2f size) {
- band_pos = pos;
- band_size = size;
- }
-
- void RoundedRectangle::set_band_color(sf::Color color) {
- band_color = color;
- }
-
void RoundedRectangle::draw(sf::RenderTarget &target) {
+ const sf::Vector2f resolution = size + sf::Vector2f(shadow_radius*2.0f, shadow_radius*2.0f);
+
// TODO: Remove these for optimizations. Also do the same in other places where setUniform is called
- rounded_rectangle_shader->setUniform("radius", radius);
- rounded_rectangle_shader->setUniform("band_pos", band_pos + sf::Vector2f(shadow_radius, shadow_radius));
- rounded_rectangle_shader->setUniform("band_size", band_size);
- rounded_rectangle_shader->setUniform("band_color", sf::Glsl::Vec4(band_color.r/255.0f, band_color.g/255.0f, band_color.b/255.0f, band_color.a/255.0f));
- rounded_rectangle_shader->setUniform("resolution", size + sf::Vector2f(shadow_radius*2.0f, shadow_radius*2.0f));
+ if(std::abs(cached_radius - radius) > 0.01f) {
+ rounded_rectangle_shader->setUniform("radius", radius);
+ cached_radius = radius;
+ }
+
+ if(std::abs(cached_resolution_x - resolution.x) > 0.01f || std::abs(cached_resolution_y - resolution.y) > 0.01f) {
+ rounded_rectangle_shader->setUniform("resolution", size + sf::Vector2f(shadow_radius*2.0f, shadow_radius*2.0f));
+ cached_resolution_x = resolution.x;
+ cached_resolution_y = resolution.y;
+ }
+
target.draw(vertices, 4, sf::Quads, rounded_rectangle_shader);
}
} \ No newline at end of file