aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authordec05eba <dec05eba@protonmail.com>2018-04-22 05:58:44 +0200
committerdec05eba <dec05eba@protonmail.com>2018-04-22 05:59:18 +0200
commit1e0e68f9cda51c881b32a54d9eece71c1428f7ac (patch)
treeb8faa1d971c245e3fcf046aa1d2daa1fa601e0f9
parent424b02609fa34175a4e2aadb95e68b3c9c8dc93c (diff)
Add video and gif support
Gif streams from url. Todo: Add play controls to video
m---------depends/odhtdb0
-rw-r--r--include/Cache.hpp14
-rw-r--r--include/Channel.hpp3
-rw-r--r--include/Chatbar.hpp9
-rw-r--r--include/FileUtil.hpp18
-rw-r--r--include/Gif.hpp47
-rw-r--r--include/Message.hpp6
-rw-r--r--include/MessageBoard.hpp1
-rw-r--r--include/MessagePart.hpp7
-rw-r--r--include/StringView.hpp62
-rw-r--r--include/Video.hpp45
-rw-r--r--project.conf4
-rw-r--r--src/Cache.cpp87
-rw-r--r--src/Channel.cpp50
-rw-r--r--src/Chatbar.cpp76
-rw-r--r--src/FileUtil.cpp35
-rw-r--r--src/Gif.cpp169
-rw-r--r--src/Message.cpp40
-rw-r--r--src/MessageBoard.cpp106
-rw-r--r--src/MessagePart.cpp8
-rw-r--r--src/Video.cpp150
-rw-r--r--src/main.cpp9
22 files changed, 806 insertions, 140 deletions
diff --git a/depends/odhtdb b/depends/odhtdb
-Subproject 59e86b8b22c5ffb925b5a68b43de4ddc92986d5
+Subproject 13718f15767db6774dbf562b6798838ad905898
diff --git a/include/Cache.hpp b/include/Cache.hpp
index 59d997a..0314e7e 100644
--- a/include/Cache.hpp
+++ b/include/Cache.hpp
@@ -14,6 +14,8 @@ namespace TinyProcessLib
namespace dchat
{
+ class Gif;
+
struct ImageByUrlResult
{
enum class Type
@@ -23,9 +25,19 @@ namespace dchat
FAILED_DOWNLOAD
};
+ ImageByUrlResult() : texture(nullptr), type(Type::DOWNLOADING), isGif(false) {}
+ ImageByUrlResult(sf::Texture *_texture, Type _type) : texture(_texture), type(_type), isGif(false) {}
+ ImageByUrlResult(Gif *_gif, Type _type) : gif(_gif), type(_type), isGif(true) {}
+
// @texture is null if @type is DOWNLOADING or FAILED_DOWNLOAD
- sf::Texture *texture;
+ union
+ {
+ sf::Texture *texture;
+ Gif *gif;
+ };
+
Type type;
+ bool isGif;
};
class Cache
diff --git a/include/Channel.hpp b/include/Channel.hpp
index fa52a4b..b70803c 100644
--- a/include/Channel.hpp
+++ b/include/Channel.hpp
@@ -13,6 +13,9 @@ namespace dchat
Channel();
~Channel();
+ User* getLocalUser();
+ MessageBoard& getMessageBoard();
+
void processEvent(const sf::Event &event);
void draw(sf::RenderWindow &window, Cache &cache);
private:
diff --git a/include/Chatbar.hpp b/include/Chatbar.hpp
index 143ebba..d24b2af 100644
--- a/include/Chatbar.hpp
+++ b/include/Chatbar.hpp
@@ -3,9 +3,13 @@
#include <SFML/Graphics/Text.hpp>
#include <SFML/Graphics/RenderWindow.hpp>
#include <SFML/Graphics/RectangleShape.hpp>
+#include <SFML/Window/Event.hpp>
+#include <SFML/System/Clock.hpp>
namespace dchat
{
+ class Channel;
+
class Chatbar
{
public:
@@ -20,11 +24,16 @@ namespace dchat
void moveCaretLeft();
void moveCaretRight();
+ bool isFocused() const;
+
+ void processEvent(const sf::Event &event, Channel *channel);
void draw(sf::RenderWindow &window);
private:
sf::Text text;
sf::RectangleShape background;
int caretIndex;
sf::Vector2f caretOffset;
+ sf::Clock blinkTimer;
+ bool focused;
};
}
diff --git a/include/FileUtil.hpp b/include/FileUtil.hpp
new file mode 100644
index 0000000..0cfc808
--- /dev/null
+++ b/include/FileUtil.hpp
@@ -0,0 +1,18 @@
+#pragma once
+
+#include "StringView.hpp"
+#include <boost/filesystem/path.hpp>
+#include <stdexcept>
+
+namespace dchat
+{
+ class FileException : public std::runtime_error
+ {
+ public:
+ FileException(const std::string &errMsg) : std::runtime_error(errMsg) {}
+ };
+
+ // Throws FileException on error.
+ // Returned value is allocated with malloc and should be free'd by caller.
+ StringView getFileContent(const boost::filesystem::path &filepath);
+}
diff --git a/include/Gif.hpp b/include/Gif.hpp
new file mode 100644
index 0000000..1a69a52
--- /dev/null
+++ b/include/Gif.hpp
@@ -0,0 +1,47 @@
+#pragma once
+
+#include "StringView.hpp"
+#include <SFML/Graphics/RenderWindow.hpp>
+#include <SFML/Graphics/Texture.hpp>
+#include <SFML/Graphics/Sprite.hpp>
+#include <SFML/System/Clock.hpp>
+#include <boost/filesystem/path.hpp>
+#include <stdexcept>
+extern "C"
+{
+#include <libnsgif.h>
+}
+
+namespace dchat
+{
+ class GifLoadException : public std::runtime_error
+ {
+ public:
+ GifLoadException(const std::string &errMsg) : std::runtime_error(errMsg) {}
+ };
+
+ class Gif
+ {
+ public:
+ // Throws GifLoadException on error
+ Gif(const boost::filesystem::path &filepath);
+ Gif(StringView &&fileContent);
+ ~Gif();
+
+ void setPosition(const sf::Vector2f &position);
+ void setSize(const sf::Vector2f &size);
+ void draw(sf::RenderWindow &window);
+
+ static bool isDataGif(const StringView &data);
+ private:
+ void init();
+ private:
+ gif_animation gif;
+ StringView fileContent;
+ unsigned int currentFrame;
+ sf::Sprite sprite;
+ sf::Texture texture;
+ double timeElapsedCs;
+ sf::Clock frameTimer;
+ };
+}
diff --git a/include/Message.hpp b/include/Message.hpp
index efc1f4c..7cd7fdf 100644
--- a/include/Message.hpp
+++ b/include/Message.hpp
@@ -13,10 +13,12 @@ namespace dchat
Message(User *user);
virtual ~Message();
- void addText(const std::string &text);
- void addImage(const std::string &url);
+ void addText(const std::string &text, bool newLine = true);
+ void addEmoji(const std::string &url, bool newLine = true);
std::vector<MessagePart*>& getParts();
+ static Message* buildFromString(User *user, const std::string &str);
+
const User *user;
private:
std::vector<MessagePart*> messageParts;
diff --git a/include/MessageBoard.hpp b/include/MessageBoard.hpp
index 105d675..c510164 100644
--- a/include/MessageBoard.hpp
+++ b/include/MessageBoard.hpp
@@ -22,7 +22,6 @@ namespace dchat
void draw(sf::RenderWindow &window, Cache &cache);
private:
sf::RenderTexture staticContentTexture;
- bool useStaticContentTexture;
bool dirty;
bool selectingText;
bool leftMouseButtonPressed;
diff --git a/include/MessagePart.hpp b/include/MessagePart.hpp
index d0a0a03..00544fb 100644
--- a/include/MessagePart.hpp
+++ b/include/MessagePart.hpp
@@ -16,7 +16,7 @@ namespace dchat
EMOJI
};
- MessagePart(Type _type) : type(_type) {}
+ MessagePart(Type _type, bool _newLine) : type(_type), newLine(_newLine) {}
virtual ~MessagePart(){}
static float getSizeScaled();
@@ -24,12 +24,13 @@ namespace dchat
virtual sf::Vector2f getSize() const = 0;
const Type type;
+ bool newLine;
};
class MessagePartText : public MessagePart
{
public:
- MessagePartText(const std::string &text);
+ MessagePartText(const std::string &text, bool newLine);
static float getFontSizeScaled();
virtual sf::Vector2f getPosition() const override;
@@ -41,7 +42,7 @@ namespace dchat
class MessagePartEmoji : public MessagePart
{
public:
- MessagePartEmoji(const std::string &url);
+ MessagePartEmoji(const std::string &url, bool newLine);
static float getHeightScaled();
virtual sf::Vector2f getPosition() const override;
diff --git a/include/StringView.hpp b/include/StringView.hpp
new file mode 100644
index 0000000..3293358
--- /dev/null
+++ b/include/StringView.hpp
@@ -0,0 +1,62 @@
+#pragma once
+
+#include "types.hpp"
+#include <cstring>
+#include <cassert>
+
+namespace dchat
+{
+ class StringView
+ {
+ public:
+ StringView() : data(nullptr), size(0)
+ {
+
+ }
+
+ StringView(const StringView &other) : data(other.data), size(other.size)
+ {
+
+ }
+
+ StringView(const char *_data) : data(_data), size(strlen(_data))
+ {
+
+ }
+
+ StringView(const char *_data, usize _size) : data(_data), size(_size)
+ {
+
+ }
+
+ StringView operator = (const StringView &other)
+ {
+ StringView result(other.data, other.size);
+ return result;
+ }
+
+ StringView(StringView &&other)
+ {
+ data = other.data;
+ size = other.size;
+
+ other.data = nullptr;
+ other.size = 0;
+ }
+
+ bool equals(const StringView &other) const
+ {
+ if(size != other.size) return false;
+ return memcmp(data, other.data, size) == 0;
+ }
+
+ char operator [] (usize index) const
+ {
+ assert(index < size);
+ return data[index];
+ }
+
+ const char *data;
+ usize size;
+ };
+}
diff --git a/include/Video.hpp b/include/Video.hpp
new file mode 100644
index 0000000..f148c07
--- /dev/null
+++ b/include/Video.hpp
@@ -0,0 +1,45 @@
+#pragma once
+
+#include <SFML/Graphics/RenderWindow.hpp>
+#include <SFML/Graphics/Texture.hpp>
+#include <SFML/Graphics/Sprite.hpp>
+#include <SFML/Window/Context.hpp>
+#include <thread>
+#include <mutex>
+#include <atomic>
+#include <stdexcept>
+
+class mpv_handle;
+class mpv_render_context;
+
+namespace dchat
+{
+ class VideoInitializationException : public std::runtime_error
+ {
+ public:
+ VideoInitializationException(const std::string &errMsg) : std::runtime_error(errMsg) {}
+ };
+
+ class Video
+ {
+ public:
+ // Throws VideoInitializationException on error
+ Video(unsigned int width, unsigned int height, const char *file, bool loop = false);
+ ~Video();
+
+ void setPosition(float x, float y);
+ void draw(sf::RenderWindow &window);
+
+ // This counter is incremented when mpv wants to redraw content
+ std::atomic_int redrawCounter;
+ private:
+ sf::Context context;
+ mpv_handle *mpv;
+ mpv_render_context *mpvGl;
+ std::thread renderThread;
+ std::mutex renderMutex;
+ sf::Sprite sprite;
+ sf::Texture texture;
+ sf::Uint8 *textureBuffer;
+ };
+}
diff --git a/project.conf b/project.conf
index d8e64f9..f2d4c9f 100644
--- a/project.conf
+++ b/project.conf
@@ -10,3 +10,7 @@ sfml-graphics = "2.4.2"
sfml-system = "2.4.2"
boost-filesystem = "1.66.0"
tiny-process = "2.0.0"
+mpv = "1.100.0"
+gl = "18.0"
+x11 = "1.6.5"
+libnsgif = "0.2.0"
diff --git a/src/Cache.cpp b/src/Cache.cpp
index ba57d4c..accd0c4 100644
--- a/src/Cache.cpp
+++ b/src/Cache.cpp
@@ -1,6 +1,8 @@
#include "../include/Cache.hpp"
#include "../include/env.hpp"
#include "../include/ResourceCache.hpp"
+#include "../include/FileUtil.hpp"
+#include "../include/Gif.hpp"
#include <boost/filesystem/convenience.hpp>
#include <unordered_map>
#include <process.hpp>
@@ -58,6 +60,40 @@ namespace dchat
return dchatHomeDir;
}
+ ImageByUrlResult loadImageFromFile(const boost::filesystem::path &filepath)
+ {
+ try
+ {
+ StringView fileContent = getFileContent(filepath);
+ if(Gif::isDataGif(fileContent))
+ {
+ Gif *gif = new Gif(move(fileContent));
+ return { gif, ImageByUrlResult::Type::CACHED };
+ }
+ else
+ {
+ sf::Texture *texture = new sf::Texture();
+ if(texture->loadFromMemory(fileContent.data, fileContent.size))
+ {
+ delete fileContent.data;
+ texture->setSmooth(true);
+ texture->generateMipmap();
+ return { texture, ImageByUrlResult::Type::CACHED };
+ }
+ delete fileContent.data;
+ }
+ }
+ catch(FileException &e)
+ {
+
+ }
+ catch(FailedToLoadResourceException &e)
+ {
+
+ }
+ return { (sf::Texture*)nullptr, ImageByUrlResult::Type::FAILED_DOWNLOAD };
+ }
+
Cache::Cache()
{
downloadWaitThread = thread([this]
@@ -71,34 +107,24 @@ namespace dchat
if(it->process->try_get_exit_status(exitStatus))
{
bool failed = exitStatus != 0;
- ImageByUrlResult &imageByUrlResult = imageUrlCache[it->url];
-
if(!failed)
{
boost::filesystem::path filepath = getDchatDir();
odhtdb::Hash urlHash(it->url.data(), it->url.size());
filepath /= urlHash.toString();
-
- try
- {
- sf::Texture *texture = ResourceCache::getTexture(filepath.string());
- imageByUrlResult.texture = texture;
- imageByUrlResult.type = ImageByUrlResult::Type::CACHED;
- printf("Image downloaded from url: %s, texture: %u\n", it->url.c_str(), texture);
- }
- catch(FailedToLoadResourceException &e)
+
+ ImageByUrlResult imageByUrlResult = loadImageFromFile(filepath);
+ imageUrlCache[it->url] = imageByUrlResult;
+ switch(imageByUrlResult.type)
{
- fprintf(stderr, "%s\n", e.what());
- failed = true;
+ case ImageByUrlResult::Type::CACHED:
+ printf("Downloaded image from url: %s\n", it->url.c_str());
+ break;
+ case ImageByUrlResult::Type::FAILED_DOWNLOAD:
+ printf("Failed to download and load image from url: %s\n", it->url.c_str());
+ break;
}
}
-
- if(failed)
- {
- imageByUrlResult.type = ImageByUrlResult::Type::FAILED_DOWNLOAD;
- fprintf(stderr, "Image download failed for url: %s\n", it->url.c_str());
- }
-
it = imageDownloadProcesses.erase(it);
}
else
@@ -127,24 +153,15 @@ namespace dchat
odhtdb::Hash urlHash(url.data(), url.size());
filepath /= urlHash.toString();
- // Check if file exists because we dont want sfml spam with "Failed to load image""...
- if(boost::filesystem::exists(filepath))
+ ImageByUrlResult imageByUrlResult = loadImageFromFile(filepath);
+ if(imageByUrlResult.type == ImageByUrlResult::Type::CACHED)
{
- try
- {
- sf::Texture *texture = ResourceCache::getTexture(filepath.string());
- ImageByUrlResult result { texture, ImageByUrlResult::Type::CACHED };
- imageUrlCache[url] = result;
- printf("Loading image from file cache: %s\n", url.c_str());
- return result;
- }
- catch(FailedToLoadResourceException &e)
- {
-
- }
+ imageUrlCache[url] = imageByUrlResult;
+ printf("Loaded image from file cache: %s, is gif: %s\n", url.c_str(), imageByUrlResult.isGif ? "yes" : "no");
+ return imageByUrlResult;
}
- ImageByUrlResult result { nullptr, ImageByUrlResult::Type::DOWNLOADING };
+ ImageByUrlResult result((sf::Texture*)nullptr, ImageByUrlResult::Type::DOWNLOADING);
imageUrlCache[url] = result;
string downloadLimitBytesStr = to_string(downloadLimitBytes);
diff --git a/src/Channel.cpp b/src/Channel.cpp
index 664e395..5e81f37 100644
--- a/src/Channel.cpp
+++ b/src/Channel.cpp
@@ -11,14 +11,15 @@ namespace dchat
{
{
Message *message = new Message(&localOfflineUser);
- message->addText(u8"hello, worldåäö1!");
- message->addImage("https://discordemoji.com/assets/emoji/playtime.png");
+ message->addText(u8"hello, worldåäö1!", false);
+ message->addEmoji("https://discordemoji.com/assets/emoji/playtime.png");
messageBoard.addMessage(message);
}
{
Message *message = new Message(&localOfflineUser);
- message->addText(u8"hello, world2!");
+ message->addText(u8"hello, world2!", false);
+ message->addEmoji("https://discordemoji.com/assets/emoji/Feels3DMan.gif");
messageBoard.addMessage(message);
}
@@ -34,40 +35,19 @@ namespace dchat
}
+ User* Channel::getLocalUser()
+ {
+ return &localOfflineUser;
+ }
+
+ MessageBoard& Channel::getMessageBoard()
+ {
+ return messageBoard;
+ }
+
void Channel::processEvent(const sf::Event &event)
{
- if(event.type == sf::Event::TextEntered)
- {
- if(event.text.unicode == 8) // backspace
- chatbar.removePreviousChar();
- else if(event.text.unicode == 13) // enter
- {
- Message *message = new Message(&localOfflineUser);
- auto chatbarMsgUtf8 = chatbar.getString().toUtf8();
- string msg;
- msg.resize(chatbarMsgUtf8.size());
- memcpy(&msg[0], chatbarMsgUtf8.data(), chatbarMsgUtf8.size());
-
- message->addText(msg);
- messageBoard.addMessage(message);
- chatbar.clear();
- }
- else if(event.text.unicode == 127) // delete
- {
- chatbar.removeNextChar();
- }
- else
- {
- chatbar.addChar(event.text.unicode);
- }
- }
- else if(event.type == sf::Event::KeyPressed)
- {
- if(event.key.code == sf::Keyboard::Left)
- chatbar.moveCaretLeft();
- else if(event.key.code == sf::Keyboard::Right)
- chatbar.moveCaretRight();
- }
+ chatbar.processEvent(event, this);
messageBoard.processEvent(event);
}
diff --git a/src/Chatbar.cpp b/src/Chatbar.cpp
index 14edba7..bdfc75d 100644
--- a/src/Chatbar.cpp
+++ b/src/Chatbar.cpp
@@ -1,7 +1,9 @@
#include "../include/Chatbar.hpp"
#include "../include/ResourceCache.hpp"
#include "../include/Settings.hpp"
+#include "../include/Channel.hpp"
#include <cmath>
+#include <cstring>
using namespace std;
@@ -10,10 +12,13 @@ namespace dchat
const float FONT_SIZE = 24;
const float BOX_PADDING_X = 15.0f;
const float BOX_PADDING_Y = 5.0f;
+ const int BLINK_TIME_VISIBLE_MS = 500;
+ const int BLINK_TIME_INVISIBLE_MS = 500;
Chatbar::Chatbar() :
text("", ResourceCache::getFont("fonts/Roboto-Regular.ttf"), FONT_SIZE * Settings::getScaling()),
- caretIndex(0)
+ caretIndex(0),
+ focused(true)
{
text.setFillColor(sf::Color(240, 240, 240));
background.setFillColor(sf::Color(60, 60, 60));
@@ -26,6 +31,7 @@ namespace dchat
text.setString(str);
++caretIndex;
caretOffset = text.findCharacterPos(caretIndex) - text.getPosition();
+ blinkTimer.restart();
}
const sf::String& Chatbar::getString() const
@@ -43,6 +49,7 @@ namespace dchat
--caretIndex;
caretOffset = text.findCharacterPos(caretIndex) - text.getPosition();
}
+ blinkTimer.restart();
}
void Chatbar::removeNextChar()
@@ -53,6 +60,7 @@ namespace dchat
str.erase(caretIndex);
text.setString(str);
}
+ blinkTimer.restart();
}
void Chatbar::clear()
@@ -61,6 +69,7 @@ namespace dchat
caretIndex = 0;
caretOffset.x = 0.0f;
caretOffset.y = 0.0f;
+ blinkTimer.restart();
}
void Chatbar::moveCaretLeft()
@@ -68,12 +77,64 @@ namespace dchat
caretIndex = max(0, caretIndex - 1);
// TODO: Use glyph size to optimize this, no need to iterate all glyphs
caretOffset = text.findCharacterPos(caretIndex) - text.getPosition();
+ blinkTimer.restart();
}
void Chatbar::moveCaretRight()
{
caretIndex = min((int)text.getString().getSize(), caretIndex + 1);
caretOffset = text.findCharacterPos(caretIndex) - text.getPosition();
+ blinkTimer.restart();
+ }
+
+ bool Chatbar::isFocused() const
+ {
+ return focused;
+ }
+
+ void Chatbar::processEvent(const sf::Event &event, Channel *channel)
+ {
+ if(!focused) return;
+
+ if(event.type == sf::Event::TextEntered)
+ {
+ if(event.text.unicode == 8) // backspace
+ removePreviousChar();
+ else if(event.text.unicode == 13) // enter
+ {
+ if(!getString().isEmpty())
+ {
+ if(sf::Keyboard::isKeyPressed(sf::Keyboard::Key::LShift) || sf::Keyboard::isKeyPressed(sf::Keyboard::Key::RShift))
+ {
+ addChar('\n');
+ }
+ else
+ {
+ auto chatbarMsgUtf8 = getString().toUtf8();
+ string msg;
+ msg.resize(chatbarMsgUtf8.size());
+ memcpy(&msg[0], chatbarMsgUtf8.data(), chatbarMsgUtf8.size());
+ channel->getMessageBoard().addMessage(Message::buildFromString(channel->getLocalUser(), msg));
+ clear();
+ }
+ }
+ }
+ else if(event.text.unicode == 127) // delete
+ {
+ removeNextChar();
+ }
+ else
+ {
+ addChar(event.text.unicode);
+ }
+ }
+ else if(event.type == sf::Event::KeyPressed)
+ {
+ if(event.key.code == sf::Keyboard::Left)
+ moveCaretLeft();
+ else if(event.key.code == sf::Keyboard::Right)
+ moveCaretRight();
+ }
}
void Chatbar::draw(sf::RenderWindow &window)
@@ -89,8 +150,15 @@ namespace dchat
window.draw(background);
window.draw(text);
- sf::RectangleShape caretShape(sf::Vector2f(2.0f, backgroundSize.y - BOX_PADDING_Y * 2.0f));
- caretShape.setPosition(floor(text.getPosition().x + caretOffset.x), backgroundPos.y + BOX_PADDING_Y);
- window.draw(caretShape);
+ int blinkElapsedTime = blinkTimer.getElapsedTime().asMilliseconds();
+ if(focused && blinkElapsedTime <= BLINK_TIME_VISIBLE_MS)
+ {
+ sf::RectangleShape caretShape(sf::Vector2f(2.0f, backgroundSize.y - BOX_PADDING_Y * 2.0f));
+ caretShape.setPosition(floor(text.getPosition().x + caretOffset.x), (caretOffset.y + backgroundPos.y + BOX_PADDING_Y));
+ window.draw(caretShape);
+ }
+
+ if(blinkElapsedTime > BLINK_TIME_VISIBLE_MS + BLINK_TIME_INVISIBLE_MS)
+ blinkTimer.restart();
}
}
diff --git a/src/FileUtil.cpp b/src/FileUtil.cpp
new file mode 100644
index 0000000..53687dd
--- /dev/null
+++ b/src/FileUtil.cpp
@@ -0,0 +1,35 @@
+#include "../include/FileUtil.hpp"
+#include "../include/env.hpp"
+#include <cstdio>
+
+using namespace std;
+
+namespace dchat
+{
+ StringView getFileContent(const boost::filesystem::path &filepath)
+ {
+#if OS_FAMILY == OS_FAMILY_POSIX
+ FILE *file = fopen(filepath.c_str(), "rb");
+#else
+ FILE *file = _wfopen(filepath.c_str(), L"rb");
+#endif
+ if(!file)
+ {
+ int error = errno;
+ string errMsg = "Failed to open file: ";
+ errMsg += filepath.string();
+ errMsg += "; reason: ";
+ errMsg += strerror(error);
+ throw FileException(errMsg);
+ }
+
+ fseek(file, 0, SEEK_END);
+ size_t fileSize = ftell(file);
+ fseek(file, 0, SEEK_SET);
+
+ char *fileData = new char[fileSize];
+ fread(fileData, 1, fileSize, file);
+ fclose(file);
+ return { fileData, fileSize };
+ }
+}
diff --git a/src/Gif.cpp b/src/Gif.cpp
new file mode 100644
index 0000000..3f7216e
--- /dev/null
+++ b/src/Gif.cpp
@@ -0,0 +1,169 @@
+#include "../include/Gif.hpp"
+#include "../include/FileUtil.hpp"
+
+using namespace std;
+
+namespace dchat
+{
+ void* bitmapCreate(int width, int height)
+ {
+ return calloc(width * height, 4);
+ }
+
+ void bitmapDestroy(void *bitmap)
+ {
+ free(bitmap);
+ }
+
+ unsigned char* bitmapGetBuffer(void *bitmap)
+ {
+ return (unsigned char*)bitmap;
+ }
+
+ void bitmapSetOpaque(void *bitmap, bool opaque)
+ {
+
+ }
+
+ bool bitmapTestOpaque(void *bitmap)
+ {
+ return false;
+ }
+
+ void bitmapModified(void *bitmap)
+ {
+
+ }
+
+ const char* gifResultToString(gif_result code)
+ {
+ switch(code)
+ {
+ case GIF_INSUFFICIENT_FRAME_DATA:
+ return "GIF_INSUFFICIENT_FRAME_DATA";
+ case GIF_FRAME_DATA_ERROR:
+ return "GIF_FRAME_DATA_ERROR";
+ case GIF_INSUFFICIENT_DATA:
+ return "GIF_INSUFFICIENT_DATA";
+ case GIF_DATA_ERROR:
+ return "GIF_DATA_ERROR";
+ case GIF_INSUFFICIENT_MEMORY:
+ return "GIF_INSUFFICIENT_MEMORY";
+ default:
+ return "Unknown gif result code";
+ }
+ }
+
+ Gif::Gif(const boost::filesystem::path &filepath) :
+ currentFrame(0),
+ timeElapsedCs(0.0)
+ {
+ try
+ {
+ fileContent = getFileContent(filepath);
+ }
+ catch(FileException &e)
+ {
+ throw GifLoadException(e.what());
+ }
+ init();
+ }
+
+ Gif::Gif(StringView &&_fileContent) :
+ fileContent(move(_fileContent)),
+ currentFrame(0),
+ timeElapsedCs(0.0)
+ {
+ init();
+ }
+
+ void Gif::init()
+ {
+ gif_bitmap_callback_vt bitmapCallbacks =
+ {
+ bitmapCreate,
+ bitmapDestroy,
+ bitmapGetBuffer,
+ bitmapSetOpaque,
+ bitmapTestOpaque,
+ bitmapModified
+ };
+
+ gif_create(&gif, &bitmapCallbacks);
+
+ gif_result code;
+ do
+ {
+ code = gif_initialise(&gif, fileContent.size, (unsigned char*)fileContent.data);
+ if(code != GIF_OK && code != GIF_WORKING)
+ {
+ string errMsg = "Failed to initialize gif, reason: ";
+ errMsg += gifResultToString(code);
+ throw GifLoadException(errMsg);
+ }
+ }
+ while(code != GIF_OK);
+
+ if(!texture.create(gif.width, gif.height))
+ throw GifLoadException("Failed to create texture for gif");
+
+ sprite.setTexture(texture, true);
+ }
+
+ Gif::~Gif()
+ {
+ gif_finalise(&gif);
+ delete fileContent.data;
+ }
+
+ void Gif::setPosition(const sf::Vector2f &position)
+ {
+ sprite.setPosition(position);
+ }
+
+ void Gif::setSize(const sf::Vector2f &size)
+ {
+ sf::Vector2u textureSize = sprite.getTexture()->getSize();
+ sprite.setScale(size.x / (float)textureSize.x, size.y / (float)textureSize.y);
+ }
+
+ void Gif::draw(sf::RenderWindow &window)
+ {
+ double frameDeltaCs = (double)frameTimer.getElapsedTime().asMilliseconds() * 0.1; // Centisecond
+ frameTimer.restart();
+ timeElapsedCs += frameDeltaCs;
+
+ unsigned char *image = nullptr;
+ u32 startFrame = currentFrame;
+ while(true)
+ {
+ int i = currentFrame % gif.frame_count;
+ gif_result code = gif_decode_frame(&gif, i);
+ if(code != GIF_OK)
+ printf("Warning: gif_decode_frame: %s\n", gifResultToString(code));
+
+ gif_frame &frame = gif.frames[i];
+ // frame_delay is in centiseconds
+ double frameDelay = (double)frame.frame_delay;
+ if(timeElapsedCs >= frameDelay)
+ timeElapsedCs -= frameDelay;
+ else
+ break;
+
+ image = (unsigned char*)gif.frame_image;
+ ++currentFrame;
+ }
+
+ if(currentFrame != startFrame)
+ {
+ texture.update(image);
+ sprite.setTexture(texture, true);
+ }
+ window.draw(sprite);
+ }
+
+ bool Gif::isDataGif(const StringView &data)
+ {
+ return data.size >= 6 && (memcmp(data.data, "GIF87a", 6) == 0 || memcmp(data.data, "GIF89a", 6) == 0);
+ }
+}
diff --git a/src/Message.cpp b/src/Message.cpp
index 2740c11..19630b3 100644
--- a/src/Message.cpp
+++ b/src/Message.cpp
@@ -1,4 +1,5 @@
#include "../include/Message.hpp"
+#include "../include/StringView.hpp"
using namespace std;
@@ -18,18 +19,49 @@ namespace dchat
}
}
- void Message::addText(const string &text)
+ void Message::addText(const string &text, bool newLine)
{
- messageParts.push_back(new MessagePartText(text));
+ messageParts.push_back(new MessagePartText(text, newLine));
}
- void Message::addImage(const string &url)
+ void Message::addEmoji(const string &url, bool newLine)
{
- messageParts.push_back(new MessagePartEmoji(url));
+ messageParts.push_back(new MessagePartEmoji(url, newLine));
}
vector<MessagePart*>& Message::getParts()
{
return messageParts;
}
+
+ StringView getNextNewLine(const StringView &str)
+ {
+ for(usize i = 0; i < str.size; ++i)
+ {
+ if(str[i] == '\n')
+ return StringView(str.data, i);
+ }
+ return StringView();
+ }
+
+ Message* Message::buildFromString(User *user, const std::string &str)
+ {
+ Message *message = new Message(user);
+ usize strOffset = 0;
+ while(strOffset < str.size())
+ {
+ usize foundIndex = str.find('\n', strOffset);
+ usize lineEnd = foundIndex;
+ if(foundIndex == string::npos)
+ lineEnd = str.size();
+
+ message->addText(str.substr(strOffset, lineEnd - strOffset), foundIndex != string::npos);
+
+ if(foundIndex == string::npos)
+ break;
+ else
+ strOffset = lineEnd + 1;
+ }
+ return message;
+ }
}
diff --git a/src/MessageBoard.cpp b/src/MessageBoard.cpp
index 575ae5f..d39e8be 100644
--- a/src/MessageBoard.cpp
+++ b/src/MessageBoard.cpp
@@ -1,6 +1,7 @@
#include "../include/MessageBoard.hpp"
#include "../include/Settings.hpp"
#include "../include/ResourceCache.hpp"
+#include "../include/Gif.hpp"
#include <SFML/Graphics/RectangleShape.hpp>
#include <SFML/Graphics/Sprite.hpp>
#include <SFML/Window/Mouse.hpp>
@@ -32,7 +33,8 @@ namespace dchat
void MessageBoard::updateStaticContentTexture(const sf::Vector2u &newSize)
{
- useStaticContentTexture = staticContentTexture.create(newSize.x, newSize.y);
+ if(!staticContentTexture.create(newSize.x, newSize.y))
+ throw std::runtime_error("Failed to create render target for message board!");
dirty = true;
}
@@ -86,32 +88,15 @@ namespace dchat
void MessageBoard::draw(sf::RenderWindow &window, Cache &cache)
{
- sf::RenderTarget *renderTarget = nullptr;
- if(useStaticContentTexture)
- {
- renderTarget = &staticContentTexture;
- if(window.getSize() != staticContentTexture.getSize())
- updateStaticContentTexture(window.getSize());
- }
- else
- {
- renderTarget = &window;
- dirty = true;
- }
+ auto windowSize = window.getSize();
+ sf::Vector2u backgroundSize(floor(windowSize.x * 0.7f), floor(windowSize.y));
+ sf::Vector2f backgroundPos(floor(windowSize.x * 0.5f - backgroundSize.x * 0.5f), 0.0f);
- auto renderTargetSize = renderTarget->getSize();
+ if(backgroundSize != staticContentTexture.getSize())
+ updateStaticContentTexture(backgroundSize);
- if(useStaticContentTexture)
- {
- if(dirty)
- staticContentTexture.clear(BACKGROUND_COLOR);
- }
- else
- {
- sf::RectangleShape background(sf::Vector2f(renderTargetSize.x, renderTargetSize.y));
- background.setFillColor(BACKGROUND_COLOR);
- renderTarget->draw(background);
- }
+ if(dirty)
+ staticContentTexture.clear(sf::Color::Transparent);
const sf::Font &usernameFont = ResourceCache::getFont("fonts/Roboto-Regular.ttf");
@@ -122,9 +107,11 @@ namespace dchat
usernameText.setFillColor(sf::Color(15, 192, 252));
usernameText.setPosition(position);
if(dirty)
- renderTarget->draw(usernameText);
+ staticContentTexture.draw(usernameText);
position.y += usernameText.getCharacterSize() + USERNAME_PADDING_BOTTOM;
+ int index = 0;
+ int numParts = message->getParts().size();
for(MessagePart *messagePart : message->getParts())
{
switch(messagePart->type)
@@ -136,36 +123,50 @@ namespace dchat
messagePartText->text.setCharacterSize(MessagePartText::getFontSizeScaled());
messagePartText->text.setPosition(floor(position.x), floor(position.y + MessagePart::getSizeScaled() * 0.5f - MessagePartText::getFontSizeScaled() * 0.5f));
if(dirty)
- renderTarget->draw(messagePartText->text);
+ staticContentTexture.draw(messagePartText->text);
position.x += messagePartText->text.getLocalBounds().width;
break;
}
case MessagePart::Type::EMOJI:
{
MessagePartEmoji *messagePartEmoji = static_cast<MessagePartEmoji*>(messagePart);
- // Emoji is dirty when it's created, but render target can become dirty after emoji has been added, so we need to set emoji as dirty then
- if(dirty)
- messagePartEmoji->dirty = true;
- auto imageByUrlResult = cache.getImageByUrl(messagePartEmoji->url, 1024 * 512);
position.x += 5.0f;
- if(imageByUrlResult.texture)
+ auto imageByUrlResult = cache.getImageByUrl(messagePartEmoji->url, 1024 * 512);
+ bool imageDrawn = false;
+ if(imageByUrlResult.isGif && imageByUrlResult.gif)
{
- // TODO: Verify this doesn't cause lag
- messagePartEmoji->sprite.setTexture(*imageByUrlResult.texture, true);
- sf::Vector2f spriteSize(MessagePartEmoji::getHeightScaled(), MessagePartEmoji::getHeightScaled());
- messagePartEmoji->sprite.setScale(spriteSize.x / (float)imageByUrlResult.texture->getSize().x, spriteSize.y / (float)imageByUrlResult.texture->getSize().y);
- messagePartEmoji->sprite.setPosition(floor(position.x), floor(position.y + MessagePart::getSizeScaled() * 0.5f - MessagePartEmoji::getHeightScaled() * 0.5f));
- if(messagePartEmoji->dirty)
+ sf::Vector2f pos(backgroundPos.x + floor(position.x), backgroundPos.y + floor(position.y + MessagePart::getSizeScaled() * 0.5f - MessagePartEmoji::getHeightScaled() * 0.5f));
+ imageByUrlResult.gif->setPosition(pos);
+ imageByUrlResult.gif->setSize(sf::Vector2f(MessagePartEmoji::getHeightScaled(), MessagePartEmoji::getHeightScaled()));
+ imageByUrlResult.gif->draw(window);
+ imageDrawn = true;
+ }
+ else
+ {
+ // Emoji is dirty when it's created, but render target can become dirty after emoji has been added, so we need to set emoji as dirty then
+ if(dirty)
+ messagePartEmoji->dirty = true;
+ if(imageByUrlResult.texture)
{
- messagePartEmoji->dirty = false;
- renderTarget->draw(messagePartEmoji->sprite);
+ // TODO: Verify this doesn't cause lag
+ messagePartEmoji->sprite.setTexture(*imageByUrlResult.texture, true);
+ sf::Vector2f spriteSize(MessagePartEmoji::getHeightScaled(), MessagePartEmoji::getHeightScaled());
+ messagePartEmoji->sprite.setScale(spriteSize.x / (float)imageByUrlResult.texture->getSize().x, spriteSize.y / (float)imageByUrlResult.texture->getSize().y);
+ messagePartEmoji->sprite.setPosition(floor(position.x), floor(position.y + MessagePart::getSizeScaled() * 0.5f - MessagePartEmoji::getHeightScaled() * 0.5f));
+ if(messagePartEmoji->dirty)
+ {
+ messagePartEmoji->dirty = false;
+ staticContentTexture.draw(messagePartEmoji->sprite);
+ }
+ imageDrawn = true;
}
}
- else
+
+ if(!imageDrawn)
{
// TODO: Replace this with a loading gif
sf::RectangleShape emojiDownloadRect(sf::Vector2f(MessagePartEmoji::getHeightScaled(), MessagePartEmoji::getHeightScaled()));
- emojiDownloadRect.setPosition(floor(position.x), floor(position.y + MessagePart::getSizeScaled() * 0.5f - MessagePartEmoji::getHeightScaled() * 0.5f));
+ emojiDownloadRect.setPosition(backgroundPos.x + floor(position.x), backgroundPos.y + floor(position.y + MessagePart::getSizeScaled() * 0.5f - MessagePartEmoji::getHeightScaled() * 0.5f));
emojiDownloadRect.setFillColor(sf::Color::White);
window.draw(emojiDownloadRect);
}
@@ -173,25 +174,33 @@ namespace dchat
break;
}
}
+
+ if(index < numParts - 1 && messagePart->newLine)
+ {
+ position.x = 0.0f;
+ position.y += MessagePart::getSizeScaled();
+ }
+ ++index;
}
position.x = 0.0f;
position.y += MessagePart::getSizeScaled() + MESSAGE_PADDING_BOTTOM;
}
- if(useStaticContentTexture)
- staticContentTexture.display();
-
+ staticContentTexture.display();
dirty = false;
- if(useStaticContentTexture)
- window.draw(sf::Sprite(staticContentTexture.getTexture()));
+ // TODO: Save this, expensive to create on fly?
+ sf::Sprite textureSprite(staticContentTexture.getTexture());
+ textureSprite.setPosition(backgroundPos);
+ window.draw(textureSprite);
if(!selectingText) return;
sf::Vector2f selectionRectStart(min((float)mousePos.x, selectingTextStart.x), min((float)mousePos.y, selectingTextStart.y));
sf::Vector2f selectionRectEnd(max((float)mousePos.x, selectingTextStart.x), max((float)mousePos.y, selectingTextStart.y));
sf::FloatRect selectionRect(selectionRectStart, selectionRectEnd - selectionRectStart);
-
+#if 0
+ // TODO: Remove this, put logic in render loop above
for(Message *message : messages)
{
float messagePartStartX = -999.0f;
@@ -260,5 +269,6 @@ namespace dchat
window.draw(selectionShape);
}
}
+#endif
}
}
diff --git a/src/MessagePart.cpp b/src/MessagePart.cpp
index 1bde138..215c239 100644
--- a/src/MessagePart.cpp
+++ b/src/MessagePart.cpp
@@ -13,8 +13,8 @@ namespace dchat
return MESSAGE_PART_SIZE * Settings::getScaling();
}
- MessagePartText::MessagePartText(const string &_text) :
- MessagePart(Type::TEXT),
+ MessagePartText::MessagePartText(const string &_text, bool _newLine) :
+ MessagePart(Type::TEXT, _newLine),
text("", ResourceCache::getFont("fonts/Roboto-Regular.ttf"), MessagePartText::getFontSizeScaled())
{
text.setString(sf::String::fromUtf8(_text.begin(), _text.end()));
@@ -35,8 +35,8 @@ namespace dchat
return sf::Vector2f(text.getLocalBounds().width, getFontSizeScaled());
}
- MessagePartEmoji::MessagePartEmoji(const string &_url) :
- MessagePart(Type::EMOJI),
+ MessagePartEmoji::MessagePartEmoji(const string &_url, bool _newLine) :
+ MessagePart(Type::EMOJI, _newLine),
url(_url),
dirty(true)
{
diff --git a/src/Video.cpp b/src/Video.cpp
new file mode 100644
index 0000000..4ef16d6
--- /dev/null
+++ b/src/Video.cpp
@@ -0,0 +1,150 @@
+#include "../include/Video.hpp"
+#include <mpv/client.h>
+#include <mpv/render_gl.h>
+#include <clocale>
+
+#include <SFML/Config.hpp>
+
+#if defined(SFML_SYSTEM_WINDOWS)
+ #ifdef _MSC_VER
+ #include <windows.h>
+ #endif
+ #include <GL/gl.h>
+ #include <GL/glx.h>
+#elif defined(SFML_SYSTEM_LINUX) || defined(SFML_SYSTEM_FREEBSD)
+ #if defined(SFML_OPENGL_ES)
+ #include <GLES/gl.h>
+ #include <GLES/glext.h>
+ #else
+ #include <GL/gl.h>
+ #endif
+ #include <GL/glx.h>
+ #define glGetProcAddress glXGetProcAddress
+#elif defined(SFML_SYSTEM_MACOS)
+ #include <OpenGL/gl.h>
+#elif defined (SFML_SYSTEM_IOS)
+ #include <OpenGLES/ES1/gl.h>
+ #include <OpenGLES/ES1/glext.h>
+#elif defined (SFML_SYSTEM_ANDROID)
+ #include <GLES/gl.h>
+ #include <GLES/glext.h>
+ // We're not using OpenGL ES 2+ yet, but we can use the sRGB extension
+ #include <GLES2/gl2ext.h>
+#endif
+
+using namespace std;
+
+namespace dchat
+{
+ void* getProcAddressMpv(void *funcContext, const char *name)
+ {
+ return (void*)glGetProcAddress((const GLubyte*)name);
+ }
+
+ void onMpvRedraw(void *rawVideo)
+ {
+ Video *video = (Video*)rawVideo;
+ ++video->redrawCounter;
+ }
+
+ Video::Video(unsigned int width, unsigned int height, const char *file, bool loop) :
+ redrawCounter(0),
+ context(sf::ContextSettings(), width, height),
+ mpv(nullptr),
+ mpvGl(nullptr),
+ textureBuffer(new sf::Uint8[width * height * 4]) // 4 = red, green, blue and alpha
+ {
+ context.setActive(true);
+
+ if(!texture.create(width, height))
+ throw VideoInitializationException("Failed to create texture for video");
+ texture.setSmooth(true);
+
+ // mpv_create requires LC_NUMERIC to be set to "C" for some reason, see mpv_create documentation
+ std::setlocale(LC_NUMERIC, "C");
+ mpv = mpv_create();
+ if(!mpv)
+ throw VideoInitializationException("Failed to create mpv handle");
+
+ if(mpv_initialize(mpv) < 0)
+ throw VideoInitializationException("Failed to initialize mpv");
+
+ mpv_opengl_init_params openglInitParams { .get_proc_address = getProcAddressMpv };
+ mpv_render_param params[] =
+ {
+ { MPV_RENDER_PARAM_API_TYPE, (void*)MPV_RENDER_API_TYPE_OPENGL },
+ { MPV_RENDER_PARAM_OPENGL_INIT_PARAMS, &openglInitParams },
+ { (mpv_render_param_type)0, nullptr }
+ };
+
+ if(mpv_render_context_create(&mpvGl, mpv, params) < 0)
+ throw VideoInitializationException("Failed to initialize mpv opengl render context");
+
+ if(loop)
+ mpv_set_option_string(mpv, "loop", "inf");
+ mpv_set_option_string(mpv, "hwdec", "auto");
+ mpv_render_context_set_update_callback(mpvGl, onMpvRedraw, this);
+
+ renderThread = thread([this, width, height]()
+ {
+ context.setActive(true);
+ while(true)
+ {
+ while(true)
+ {
+ mpv_event *mpvEvent = mpv_wait_event(mpv, 0.0);
+ if(mpvEvent->event_id == MPV_EVENT_NONE)
+ break;
+ else if(mpvEvent->event_id == MPV_EVENT_SHUTDOWN)
+ break;
+ }
+
+ if(redrawCounter > 0)
+ {
+ --redrawCounter;
+ mpv_opengl_fbo openglFbo { .fbo = 0, .w = (int)width, .h = (int)height };
+ mpv_render_param params[] =
+ {
+ { MPV_RENDER_PARAM_OPENGL_FBO, &openglFbo },
+ { (mpv_render_param_type)0, nullptr }
+ };
+
+ context.setActive(true);
+ renderMutex.lock();
+ mpv_render_context_render(mpvGl, params);
+ glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, textureBuffer);
+ texture.update(textureBuffer);
+ sprite.setTexture(texture, true);
+ renderMutex.unlock();
+ }
+ this_thread::sleep_for(chrono::milliseconds(10));
+ }
+ });
+ renderThread.detach();
+
+ const char *cmd[] = { "loadfile", file, nullptr };
+ mpv_command(mpv, cmd);
+ context.setActive(false);
+ }
+
+ Video::~Video()
+ {
+ lock_guard<mutex> lock(renderMutex);
+ delete[] textureBuffer;
+ mpv_render_context_free(mpvGl);
+ mpv_destroy(mpv);
+ if(renderThread.joinable())
+ renderThread.join();
+ }
+
+ void Video::setPosition(float x, float y)
+ {
+ sprite.setPosition(x, y);
+ }
+
+ void Video::draw(sf::RenderWindow &window)
+ {
+ lock_guard<mutex> lock(renderMutex);
+ window.draw(sprite);
+ }
+}
diff --git a/src/main.cpp b/src/main.cpp
index 284df56..3ca986f 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -4,7 +4,7 @@
#include <string>
#include <SFML/Graphics.hpp>
#include <cstring>
-#include "process.hpp"
+#include <X11/Xlib.h>
using namespace std;
using namespace dchat;
@@ -12,16 +12,19 @@ using namespace TinyProcessLib;
int main()
{
+ XInitThreads();
sf::RenderWindow window(sf::VideoMode(1920, 1080), "dchat");
window.setVerticalSyncEnabled(false);
window.setFramerateLimit(60);
+ //odhtdb::Database database("bootstrap.ring.cx", 4222, Cache::getDchatDir());
+
Cache cache;
Channel channel;
ChannelSidePanel channelSidePanel;
channelSidePanel.addChannel(&channel);
-
+
while (window.isOpen())
{
sf::Event event;
@@ -37,7 +40,7 @@ int main()
channel.processEvent(event);
}
- window.clear();
+ window.clear(sf::Color(40, 40, 40));
channel.draw(window, cache);
window.display();
}