Archive

Posts Tagged ‘OP4JS’

Smart Mobile Studio 3.0 and beyond

March 20, 2018 Leave a comment

cascade_03With Smart Mobile Studio 3.0 entering its second beta, Smart Pascal developers are set for a boost in quality, creativity and power. We have worked extremely hard on the product this past year, including a complete rewrite of all our visual controls (and I mean all). We also introduced a completely new theme engine, one that completely de-couples visual appearance from structural architecture (it also allows scripting inside the CSS theme files).

All of that could be enough for a version bump, but we didn’t stop there. Much of the sub-strata in Smart has been re-implemented. Focus has been on stability, speed and future growth. The system is now divided into a set of name-spaces (System, SmartCL, SmartNJ, Phonegap, and Espruino), making it easier to navigate between the units as well as expanding the codebase in the future.

To better understand the namespaces and why this is a good idea, let’s go through how our units are organized.

smart_namespace

The RTL is made to expand easily and preserve as much functionality as possible

  • The System namespace is the foundation. It contains clean, platform independent code. Meaning code that doesn’t rely on the DOM (browser) or runtime (node). Focus here is on universal code, and to establish common object-pascal classes.
  • Our SmartCL namespace contains visual code, meaning code and controls that targets the browser and the DOM. SmartCL rests on the System namespace and draws functionality from it. Through partial classes we also expand classes introduced in the system namespace. A good example is System.Time.pas and SmartCL.Time.pas. The latter expands the class TW3Dispatch with functionality that will only work in the DOM.
  • SmartNJ is our high-level nodejs namespace. Here you find classes with fairly complex behavior such as servers, memory buffers, processes and auxillary classes. SmartNJ draws from the system namespace just like SmartCL. This was done to avoid multiple implementations of streams, utility classes and common functions. Being able to enjoy the same functionality under all platforms is a very powerful thing.
  • Phonegap is our namespace for mobile devices. A mobile application is basically a normal visual application using SmartCL, but where you access extra functionality through phonegap. Things like access to a device’s photos, filesystem, dialogs and so on is all delegated via phonegap.
  • Espruino is a namespace for working with Espruino micro-controllers. This has been a very low-level affair so far, due to size limitation on these devices. But with our recent changes you can now, when you need to, tap into the system namespace for more demanding behavior.

As you can see there is a lot of cool stuff in Smart Mobile Studio, and our codebase is maturing nicely. With out new organization we are able to expand both horizontally and vertically without turning the codebase into a gigantic mess (the VCL being a prime example of how not to implement a multi-platform framework).

Common behavior

One of the coolest things we have added has to be the new storage device classes. As you probably know the browser has a somewhat “limited” storage mechanism. You are stuck with name-value pairs in the cache, or a filesystem that is profoundly frustrating to work with. To remedy this we took the time to implement a virtual filesystem (in memory filesystem) that emits data to the cache; we also implemented a virtual storage device stack on top of it, one for each target (!).

In short, if a target has IO capability, we have implemented a storage “driver” for it. So instead of you having to write 4-5 different storage mechanisms – you can now write the storage code once, and it works everywhere.

This is a pretty cool system because it doesn’t limit us to local device storage. We can have device classes that talk to Google-Storage, One-Drive, Dropbox and so on. It also opens up for custom storage solutions should you already have this pre-made on your server.

Database support, a quick overview

Databases have always been available in Smart Mobile Studio. We have units for WebSQL, IndexDB and SQLite. In fact, we even compiled SQLite3 from native C code to asm.js, meaning that the whole database engine is now pure JavaScript and no-longer dependant on W3C standards.

smart_db

Each DB engine is implemented according to a framework

Besides these we also have TW3Dataset which is a clean, Smart Pascal implementation of a single table dataset (somewhat inspired by Delphi’s TClientDataset). In our previous beta we upgraded TW3Dataset with a robust expression parser, meaning that you can now set filters just like Delphi does. And its all written in Smart Mobile Studio which means there are no dependencies.

 

And ofcourse, there is also direct connections to Embarcadero Datasnap servers, and Remobjects SDK servers. This is excellent if you have an already existing Delphi infrastructure.

A unified DB framework

If you were hoping for a universal DB framework in beta-2 of v3.0, sadly that will not be the case. The good news is that databases should make it into v3.2 at the latest.

Databases looks simple: table, rows and columns right? But since each database engine known to JavaScript is written different from the next, our model has to take height for these and be dynamic enough to deal with them.

The model we used with WebSQL is turning out to be the best way forward I feel, but its important to leave room for reflection and improvements.

So getting our DB framework established is a priority for us, and we have placed it on our timeline for (at the latest) v3.2. But im hoping to have it done by v3.1. So it’s a little ahead of us, but we need that time to properly evolve the framework.

Smart Desktop [a.k.a Amibian.js]

The feedback we have received on our Smart Desktop demos have been pretty overwhelming. It is also nice to know that our prototype is being used to deliver software to schools and educational centers. So our desktop is not going away!

smart_desktop

Fancy a game of Quake at 60+ fps? Web assembly rocks!

But we are not rushing into this without some thought first. The desktop will become a project type like I have written about many times before. So you will be able to create both the desktop and client applications for it. The desktop is suitable for software that requires a windowing environment (a bit like Sencha or similar frameworks). It is also brilliant for kiosk displays and as a remote application hub.

Our new storage device system came largely from Amibian, and with these now a part of our RTL we can clean up the prototype considerably!

Smart assembler

It may sound like an oxymoron, but a lab project we created while testing our parser framework (system.text.parser unit) turned into an exercise in compiler / assembler making. We implemented a virtual machine that runs instructions represented by bytecodes (fairly straight ahead stuff). It supports the most common assembler methods, vaguely inspired by the Motorolla 68k processor with a good dose of ARM thrown in for good measure.

smart_assembler

Yes that is a full parser, assembler and runtime model

If you ponder why on earth this would be interesting, consider the following: most web platforms allow for scripting by third-party developers. And by opening up for that these, the websites themselves become prone to attacks and security breaches. There is no denying that any JS based framework is very fragile when potentially hundreds of unknown developers are hacking away at it.

But what if you could offer third parties to write plugins using more traditional languages? Perhaps a dialect of pascal, a subset of basic or perhaps C#? Wouldnt that be much better? A language and (more importantly) runtime that you have 100% control over.

While our assembler, disassembler and runtime is still in its infancy (and meant as a demo and excercise), it has future potential. We also made the instructions in such a way that JIT compiling large chunks of it is possible – and the output (or codegen) can be replaced by for example web assembly.

Right now it’s just a curiosity that people can play with. But when we have more time I will implement high-level parsers and codegens that emit code via this assembler. Suddenly we have a language that runs under node.js, in the browser or any modern JS runtime engine – and its all done using nothing but Smart Mobile Studio.

Well, stay tuned for more!

Smart Pascal: Arduino and 39 other boards

January 17, 2017 Leave a comment

Did you know that you can use Smart Mobile Studio to write your Arduino controller code? People think they need to dive into C, native Delphi or (shrug) Java to work with controller boards or embedded SoC’s, but fact is – Smart Pascal is easier to set up, cheaper and a hell of lot more productive than C.

Arduino getting you down? Why bother, whip out Smart Pascal and do some mild wrapping and kill 40 birds with one stone

Arduino getting you down? Why bother, whip out Smart Pascal and do some mild wrapping and kill 40 birds with one stone

What may come as a surprice to you though is that Smart allows you to write code for no less than 40 micro-controllers and embedded devices! Yes you read right, 40 different boards from the same codebase.

Johnny number five

jfiveNode.js is the new favorite automation platform in the world of hardware, and board’s both large and small either support JavaScript directly, or at the very least try to be JS friendly. And since Smart Mobile Studio support Node.js very well (and it’s about to get even better support), that should give you some ideas about the advantages here.

The magic library is called Johnny number five (JN5) and it gives you a more or less unified API for working with GPIO and a ton of sensors. And it’s completely free!

As of writing I havent gotten around to writing the wrapper code for JN5, but the library is so clean and well-defined that it’s something that should not take more than a day to port over. You can also take a shortcut via our Typescript importer and use our import tool.

Check out the API here: http://johnny-five.io/api/

Here is a list of the sensors JN5 supports:

  • Altimeter
  • Animation
  • Barometer
  • Button
  • Compass
  • ESC
  • ESCs
  • Expander
  • Fn
  • GPS
  • Gyro
  • Hygrometer
  • IMU
  • IR.Reflect.Array
  • Joystick
  • Keypad
  • LCD
  • Led
  • Led.Digits
  • Led.Matrix
  • Led.RGB
  • Leds
  • Light
  • Motion
  • Motor
  • Motors
  • Multi
  • Piezo
  • Pin
  • Proximity
  • Relay
  • Relays
  • Sensor
  • Servo
  • Servos
  • ShiftRegister
  • Stepper
  • Switch
  • Thermometer

And enjoy one of the many tutorials here: http://johnny-five.io/articles/

Websocket, WebSocket.io and Socket.io

October 16, 2016 8 comments

Wow. Thats is a mouthfull isn’t it? Websocket, websocket.io and socket.io. So what is that all about?

In short, im finishing off the server units for Smart Mobile Studio! And I think people are going to be super pleased and happy about this one.

As you may know, node.js gives your JavaScript more or less the same functionality as classical programming languages. You get to work with sockets, talk to databases, implement protocols – all of it in JavaScript and all of it capable of doing exactly what we are doing in Delphi. That is pretty neat!

This is so fun to make and work with

This is so fun to make and work with

But that also means someone has to expose the node.js functionality in pascal form and then write thin wrappers around that. This is where Smart Mobile Studio really shines, because after six years of constant development – we have built up a very powerful codebase. Smart mobile studio is not just about compiling to JavaScript. There are 10-20 compilers out there doing that. Nope, the power of smart mobile studio is in the infrastructure we have painstakingly constructed for our customers. Hundreds of hand-written classes, units and library files. Add to that our cosy little IDE which is very nice to work with and the advantages starts piling up.

Servers, glorious node.js servers

So whats on the menu? A lot! More than I actually thought we would add in the next update. Here is a list of the server types you will be getting:

  • http server
  • websocket server
  • tcp/ip server
  • socket.io server
  • websocket.io server

To explain the differences:

Websocket is a full-duplex communication stack that allows both client and server to talk; at the same time. It also contains fancy commands for emitting messages to all connected clients, and likewise a client can commit messages to other clients (if the server allows it). You can send both text messages and binary messages. And the cool part is that the websocket engine will collect the data, deal with net-frames and spare you all the razzle you would otherwise have to do.

On top of this we have something called socket.io. This takes the whole thing one step further, allowing you to define a set of commands on the server (by name), and associate that with the code to execute when such a command is received. Socket.io also come in client-form for both browser and node.js itself. And just like the server edition you can define a set of commands for the client as well. This moves websocket into the realm of real-time messaging, where rather than wasting days and weeks coding your own protocol, you can kick back and flesh out your command-set much in the way RemObjects SDK does. Socket.io also contains clustering mechanics, but I wont cover that just yet.

Node.js leveraged for object pascal, love it

Node.js leveraged for object pascal, love it

Websocket.io is sort of third option. It’s basically websocket and socket.io glued together. You don’t have to upgrade the http socket (read up on websocket if you have no clue what that means) if you don’t want to. Websocket.io gives you pretty much exactly the same as websocket + socket.io, but from a unified API. And to be honest this is the library I use the most myself.

So long story short: you get 3 websocket based server classes to play with, all of them inheriting from TNJCustomServer, so you use the same code to work with all servers. And the speed is phenomenal! I have to pinch myself sometimes just to remember that this is kick-ass javascript running the show.

If you think websocket is “communication with training wheels” (I don’t, I frikkin love it) then a raw TCP server is probably your cup of tea. This is a perfect class to use if you need to port over some old Delphi code, if you for some reason hate XML or JSON – or perhaps you just want to dump over raw binary data as quickly as possible. Either way: it’s there, and it works brilliantly.

As you would expect I’m also doing the client side of things. We have supported websocket out of the box for a while now, and I just added a socket.io client to the codebase. I have a few alternatives to try out on the raw tcp stuff, but ultimately tcp is now allowed for HTML5. But a client and server for node.js is ofcourse on the menu.

Benefits

It’s when I sit and work with the codebase like I do now that I see just how much cool stuff we have. When doing research on a library, how to best wrap or implement it in our RTL, I naturally come across hundreds of comments by JavaScript programmers. Things like allocating memory, copying data from one buffer to the next, working with bytes and bits — a lot of these guys are having a really hard time doing classical programming.

But for Smart Pascal that’s not even an issue. We have supported memory allocation, blue-pointers and even a threading model for quite some time now. Yes, threading. Check your RTL folder and you will find it 🙂 It goes without saying that when you have memory streams, file streams, raw buffer classes, stacks, queues and proper inheritance that smart pascal gives you more than just a head-start. The benefits are astronomical in places.

And no, im not just blowing my own horn here — I use Smart Mobile Studio to create services and mobile applications on a daily basis. So I am painfully aware of what we did wrong, but also what we did right. And thankfully we got more right than wrong.

 

 

Hexlog, building a better logging system

October 15, 2016 Leave a comment

Its been quite a busy couple of weeks for me. HexLicense has finally gotten a well deserved update and (drumroll) we have ported the codebase over to Firemonkey. I have deliberately waited a while with the Firemonkey edition, because compilers usually need a couple of releases before they become stable. And the same can be said about runtime libraries (I should know, I have created a few). I wanted to use FMX earlier but, it kinda died on me so many times that I gave up. Thankfully this is no longer the case and Delphi XE Seattle is a joy to work with.

Ironwood prototype

Ironwood prototype

Also, one of the coolest things these past weeks was returning to Smart Mobile Studio as a user. It’s really quite an odd experience because you get so locked into “author” mode when you have worked on a product for so many years. So when you sit down to actually use your own program as a means to an end, it’s a very different experience from looking at it purely architecturally.

I really love what we managed to do with Smart Mobile Studio. That might sound disingenuous since it originated with me, but somehow we captured something; some sense of creative freedom that I cannot find in giant productions like Visual Studio or QT C++. Naturally I’m biased, but I’m no stranger to self-critique either. But the speed at which I knock out a HTML5 or mobile application with Smart Pascal is just way beyond anything I can do in any other devkit. The only other development platform that have the same feel to it is Mono C#, which I love because it doesn’t contain all the .. bloat, I guess is a word, that large production environments insist on giving you. I mean, fire up visual studio and you have so many options – half of them that you will never use unless you specialize within a particular dicipline – that they just get in the way.

Anyways, Hexlicense “Ironwood” was written first and foremost in Smart Mobile Studio. We have started to port the code over to Delphi to make the Xmas deadline, but honestly: I’m so glad I could use Smart to prototype this product, because it would have taken me twice as long in Freepascal or Delphi. I’m not even sure I would have bothered if Visual Studio was my only option. Seriously.

So you may be wondering: why on earth would you implement a licensing system in JavaScript? Well that’s the cool part! Right now most licensing systems are native only. Which means that if you want to really get control over your software you have to fork out for a virtual host, if not a physical server box. The price difference between a node.js hosting solution and a native hosting solution is substantial. So implementing a version of our HexLicense server in Smart Pascal + node.js will save my customers the expense. It also means they can host the server on whatever operative system they see fit. And once again, the price difference between a Linux host (Ubuntu is wonderful to work with) and Microsoft Windows is still a factor.

Coding it in Smart Pascal also opens up the door for license based access to HTML5 based applications, be they compiled to native via Phonegap or Adobe build services or just running in a browser. So once again our JavaScript virtual machine formula get’s it right. And it’s magnificent to play with. Not a day goes by that I don’t learn something new and exciting about JavaScript from object pascal, which is paradoxical. I know.

Back in the saddle

For the past few years my life has revolved around Smart Mobile Studio and the companies I have worked for (a.k.a “the day job”). It must be at least 4-5 years since I actually sat down and built products, delivered components and offered up my services as a Delphi software architect. There havent even been any time for consulting or system design. With a full day job, two kids and a product you live and breathe for there is a limit to how much you manage. Add to that my back injury 4 years ago which rendered me unable to even walk and you get the picture; Thankfully my back is getting better. I do two rounds of boxing every week (or try to) and that has done more for my back than all the doctors and their witch-craft medicine combined.

It feels so good to create new products again. Products that are doable within a reasonable time-frame and that solve real-life practical problems (as opposed to having to solve potential scenarios that havent even occurred yet: the burden of the RTL architect). So while HexLicense for VCL and FMX is in the store, Ironwood is being ported to Delphi – I have decided to wake up another component-set that I initially created for myself. One that deals with a very practical and hands-on challenge, namely: HexLog.

HexLog

Many years ago I was working for Hydro, which is the biggest oil company in Norway. Without getting to detailed (or breaking my NDA) logging was one of the problems we faced. With more than 50 Windows services communicating (read: 50 potential sources of bugs), the company’s lack of proper logging – logging in a format that made sense to people other than us developers (like system administrators, super-users and even insurance companies interested in locating where something broke down) made it almost impossible to work with the codebase.

So one day I had enough and sat down in my spare time to write a modular, thread safe, component based logging system that didn’t just “log some info”, but it did so by proxy. Meaning, that the writing mechanism(s) were isolated in separate components, abstracted from storage – which was isolated in other components. So when we needed XML logging we just hooked up the XML writer; when we needed RTF logging we hooked that up (and so on, for numerous formats).

You would think that Delphi developers in general had logging under wraps right? That is sadly not the case. Some people are very good at logging and have made it into a habit. But more often than not when I’m hired by a client – what do you think I find? Yup, plain old vanilla text-file logging. Which is fine as long as the coders have spent some time making sure the basics are in order. Sadly that is so rare it almost frightens me. Especially when I’ve upgraded software used by pharmacutical companies, doctors and organizations that really (really!) should do full journaling regardless of bugs or errors. In fact Norwegian law demands it in some cases; especially when medicine and prescription drugs are involved. If a doctor or nurse prescribes the wrong medication and a patient dies, the insurance companies will have a field day if there is no logging according to standards. And should it turn out to be the software’s fault.. well, let’s just say there is a reason only large companies operate with “within the hour response time” in their service agreements.

So what are the criteria people tend to forget?

  • File locking issues. It will happen the moment more than one process or thread targets a file
  • Making sure the information that is logged is organized by sections, visually distinct in the file
  • Making sure time and dates use UTC or ISO formatting
  • Logging the actual user, not just the program identity
  • Using mutexes and read-write synchronization when multi-threading is involved
  • Avoiding interface communication from threads without proper understanding of calling conventions and compartment schemes
  • Using memory mapped files rather than torturing the filesystem with an onslaught of IO calls
  • Use tab indentation to make the logs easier to navigate for the human eye
  • The list goes on ..

The most important oversight is not on the list, it’s actually something as simple as logging information that human beings can read, as opposed to adapting the information to what the computer wants to work with. A dentist wont know what to do with a stack-trace or “an error occurred executing MySQLQuery1.Execute”. But he or she will understand a log that states – in plain text – “could not store prescription for patient John Doe, the database reports the disk as full. An email has been sent to the administrator“.

You don’t have to be a programmer to replace a disk or move the database to a better location. But the log must make sense. It should just be there, not getting in the way yet easily accessible.

Journaling and big-data

When you are logging every action an oil-pump (and its regulators) do for 24 hours, logfiles can grow into the gigabyte range. Can you imagine one gigabyte of messy, purely technical jargon to wade through when a critical system is down? Well I can, because that’s exactly one of the things I faced 12 years ago when I was hired to “fix” a couple of bugs. A couple of bugs turned into 2 years of re-writing the entire system from scratch. Notepad crashed when it tried to load the original log-files.

The system was all written in Delphi 4, breaking every rule known to object pascal developers even back then. Like services opening forms and forms using DDE to communicate. It was a miracle that it had worked to begin with.

This is where I decided to write a journaling system that would present me, the programmer, with a unified API for writing, reading and even updating log-items stored in multiple files. So you can set a limit to how many log-items one file can contain, and when the log reaches that limit – it create a new file automatically for you. It takes care of everything in the background, keeping track of the files and their content through an index files. This solution helped solve the problem of monster log files. And you know what? That was the core bug in the system. They had used TStringList to load in the text file, append one item, then saved it back out again. Loading almost a gigabyte of raw text while trying to keep up with GPIO signals firing like mad. So yeah, logging does matter!

HexLog naturally implements this, now even faster than before due to Delphi’s dictionary classes and clever use of balanced trees. The best is that the writer mechanism applies to this type of journaling as well. So if you prefer XML thats not a problem, nor JSON, nor RTF for that matter. I’m even throwing in a PDF writer in update 1.

And did I mention both local logging and network logging? And that you can read the logs via a fancy HTML5 dashboard in any browser, both locally and remotely?

Reading more about HexLog

If you find the topic interesting and worthwhile, head over to my company website and read the release statement here: www.quartexcomponents.com. We dont take orders just yet, but it should ship out in the beginning/middle of next month.

And yes, there will be a server edition both for native Delphi and node.js, so there is a lot of value for money in this package.

 

Smart Mobile Studio, updates ohoy

August 29, 2016 2 comments

Its been a while since I’ve written much about Smart Mobile Studio. There are many reasons for this, but I figured I could write a bit now so people dont think we are standing still 🙂 So.. whats going on? Lets go through the highlights!

smart

Some of the more advanced demos include a full bytecode compiler written in Smart Mobile Studio. It may seem like overkill, but its a perfect example of just what Smart can deliver!

New team

When you have a small team you can pivot on a dime. There are no board meetings, no finding dates that suits everyone, “vacation” is a foreign word – and you can pretty much jump on ideas and just implement them straight away.

But as software grows in size and complexity, that way of working can cause more challenges than solutions. You have to establish a proper company structure, plan ahead with care, delegate tasks and do chores you may not like.

Also, teams are people. And people change, take on new jobs and move. Without much drama that is the case for us as well — and while the core of the old team is still working on Smart, we had to establish a new team with more members (and board meetings and .. well, you know how it goes).

New RTL structure

The initial RTL was built side by side with the compiler. In the beginning we didnt even have classical functions like Assigned(), so the codebase had a lot of esoteric twists and asm sections that – as the compiler grew in complexity, became redundant and caused hard to track-down bugs. The compiler is now en-par with Delphi in many ways, although without generics; It represents a whole new dialect of pascal. Where Delphi is the absolute top-dog when it comes to Windows desktop applications – tailored from scratch to fit the Windows API, Smart Pascal was made for Javascript and to interact with the javascript virtual machine environment.

But the old structure, while it served us well in knocking out Cordova Phonegap based apps quickly — just had to be refactored at some point. Javascript is Async by nature and trying to “mimic” blocking execution was starting to impact performance, order and even being able to read and understand the code (for users coming from Delphi or Freepascal).

book

Smart Mobile Studio Unleashed will be released before xmas 2016, it explains in great detail everything you need to write controls, work with memory, write services and basically use Smart Mobile Studio to its full potential.

Just think about it: We want to support mobile devices, nodejs client/server, embedded technology, Windows Universal apps and all the exciting gadgets that are coming out with pure JS interfaces. To pull that off you really need all your ducks in order and a clear separation between visual. non visual and hybrid systems must be in place.

We have solved this quite elegantly in my opinion, by the use of namespaces.

You have the system namespace which implements platform independent code (and abstract classes). Then you have the SmartCL visual framework that contains code that targets the DOM and browser. And finally you have the SmartNJ namespace that contains code adapted for NodeJS and headless frameworks. For embedded devices each will have their own namespace (like Arduino), all of them rooted in the platform independent system namespace.

This is an elegant way of solving how you approach the different platforms out there, it has less overhead than the “all in one” VCL type RTL we have fostered so far.

The downside is naturally, like C# coders live with every day, more units. But I have made sure that correlations match 1:1 as much as possible.

So where you find, for example, “system.filesystem.pas”, you will also find a corresponding “smartcl.filesystem.pas” and a “smartnj.filesystem.pas”. So once you know the units in the system namespace, its just a matter of replacing the prefix for the platform you target.

New tech

While re-factoring  whole RTL is more than enough work, we also want to get new tech into Smart Mobile Studio. A unified filesystem has been long coming, and while we have units to access pretty much every nook and crumble Javascript has to offer — a high level interface that is unified is really where my focus is now.

Storing files on mobile devices, in the browser, node or embedded really should be the same. You should write code once and it should work no matter what namespace you target. I want to shield users from some of the Javascript solutions still out there. But people should always have the option to dig into the underlying API.

Databases

Then there is databases. This has really baked my noodle. As mentioned JavaScript is not linear. Its Async by nature, which means writing a concrete API that fits all has been problematic to say the least. But I am happy to say that the code I have implemented is finally rich enough to capture all of them.

grid

The long awaited DB grid. This one handles thousands of records very fast and uses CSS GPU scrolling and effects. As always its skinnable through the stylesheet

But a DB API is just the foundation. NodeJS has drivers for almost all native databases out there, like MSSQL, MariaDB, Oracle and even good old access for that matter. Implementing these under a unified framework takes time — which is also why we needed more people to join the group.

I have covered the most essential, the built in databases supported by the browser, sqlite and also TW3Dataset — this is more than enough for mobile and browser applications. But NodeJS is a whole other ballgame, and that has really been a challenge. Allthough a very rewarding one intellectually. But NodeJS is so massive that we have to build up over time, its impossible for such a small team to deliver high-level classes for all of them in such short time.

Actions

Actions is another factor I have really wanted for a long time. In Delphi and Lazarus this makes programming so much easier, faster and elegant. And writing a Smart version of this was not hard at all. So this is in place — we only need to adjust the property inspector and automatic codegen to take height for action objects.

Tweening

The effect system in Smart is pretty cool. In fact its one of the most powerful CSS GPU based systems out there. No other system makes writing GPU powered effects so easy, yet its one of the least known aspects of Smart Mobile Studio.

scrolling

Momentum scrolling for listboxes and displays is now standard. In fact, the actual scroll code is isolated in components, so you can mix and match the scroll type you like.

The downside of effects that uses hardware, is that they have to run their course. Once started you cant really stop them, or abort the sequence mid-way. This is actually down to the browser, and even though its all defined in the WC3 standards – no browser really implements the stop feature.

To give users an alternative we have implemented a CPU only tweening system. This can be used to tween (morph is also a word used for this) anything from colors to moving elements. But as mentioned, CPU will always be slower than GPU. But with the tweening library in place, used for example in combination with Sprite3d — you have everything you need to write high-speed, silky smooth multimedia and control effects.

Components, not just controls

And yes, finally we have a pure separation between visual and non-visual components. Once established in the IDE you will be able to drag & drop non-visual controls, and we have debated the use of datamodules as well.

Since the VCL/LCL TComponent type inheritance chain is quite fat, we have avoided that. Non visual controls are called widgets, withe TW3Component now inheriting from TW3CustomWidget. This makes the inheritance chain linear – but allows you to safely write non visual components that are universal and dont collide with the namespaces scheme.

This is really cool! For instance listboxes now have a scrollmethod property where you can hook up a non-visual scroll mechanism. Momentum scroll? Row-by-Row scroll? Flat scroll? It makes it so much easier to hook things up.

Where to next?

There are a few company formalities left, but once those are in place we’ll get cracking! I personally cant wait to get going on the new IDE features. And with nodeJS becoming more and more relevant, both for embedded work and Smart devices – I think everyone will agree that it was worth the wait!

And as always — a long, long nose to those that said “it cant be done” 🙂

For those about to scroll, we salute you

June 10, 2016 1 comment

Came up with a better solution to the scrolling problem for Smart Mobile Studio. While I hate having to write code for Internet Explorer, it is nice to have a system that works everywhere. But that means more abstraction and “drivers” type classes.

The scrolling model

TW3Scrollbox and it’s variation(s) implement different types of scrolling. TW3Scrollbox has a very fast non-momentum scroll, making it perfect for displaying detailed information. But it would be nice if we could choose right?

The model I have come up with is super simple, especially in the upcoming RTL where we finally have non-visual components. I give you – TW3ScrollController

scroll_model

In the present model, TW3Scrollbox deals with scrolling directly. Actually the scrolling is implemented in the TW3ScrollContent control, mapping touch and mouse events directly. This turned out to be a remarkably fast way of moving things around. I really did not expect IE to keep up, but it’s perfectly pristine!

In the momentum-scroll example I posted, control of scrolling is handled by the container rather than content. This is very fast on webkit, and I also tried it on Microsoft mobile and Windows 10 mobile – and it’s very fast there. But for some reason the desktop Internet Explorer is slow and the content jitters a bit.

The culprit is not my code or approach, it’s actually something else. Internet Explorer and Edge are the only browsers that implements OnReSize() events. No other browser has this. In the SMS RTL we have to manually figure out when to call ReSize (based on the BeginUpdate, AddControlState, EndUpdate methods).

In the momentum scroller I use SetBounds() to keep the content within the horizontal bounds of the control. This causes an extra call to Resize every time the content moves even a pixel (even though it shouldnt, because the size doesnt change). So yeah, fixing that will make all the difference. I’m going to nail this thing once and for all, just like I did with font measurements way back.

TW3ScrollController

But isolating the code that actually deals with scrolling in separate, non visual components that you can attach to the TW3ScrollBox makes sense. Rather than hardcoding everything into a huge, spaghetti monster unit — we can now isolate different scroll methods in their own units (keeping those bytes down).
Normal per-pixel scrolling, momentum scrolling, CSS3 animation based scrolling, tween based (cpu) scrolling. It gives us some options – and allows you to implement your own variations if you find mine lacking.

I’m also giving the browser driver a much deserved overhaul. Getting the browser type and version info should be easy (and humanly readable). And since you may want to pick different scroll methods depending on the browser type — being able to check if your running on Edge for the desktop, or IE on a mobile device… well, it should be there. End of story.

iScroll

I really want iScroll to be the standard scroll library for Smart Mobile Studio, but since people feel it’s hard to use and adapt to — I may end up doing the unthinkable and re-write it in object pascal from scratch. But iScroll5 really is so much better. It has been developed and tested on a plethora of devices for six years now.

It even does things the built-in browser scrolling (for the browsers that allows this, yeah im looking at you Safari!) doesn’t deal with.

But I have enough on my plate right now, so iScroll porting will have to wait.

Momentum Scrolling

May 31, 2016 5 comments

Momentum scrolling is something we havent had as an option in the VJL directly. We excluded it initially because there were excellent JavaScript libraries especially for this (like iScroll), but in retrospect I guess it wouldnt hurt to have it in the VJL written in object pascal.

Here is a little something I slapped together the other day. Im going to make both TListbox and the ordinary content containers have an option for this.

scroller

Oooo.. sexy sexy scroller thingy!

 

Note: This supports both mouse and touch, and if you are confused about the event objects then head over to Github and snag a copy of that there. Just remove the references to units you dont have and include eventobjs.pas in your uses clause!

The call to SetInitialTransformationStyles() should be replaced with (this makes the browser mark the element for GPU, which is very fast):

    FContent.Handle.style[BrowserAPI.Prefix('transformStyle')] := 'preserve-3d';
    FContent.Handle.style[BrowserAPI.Prefix('Perspective')] := 800;
    FContent.Handle.style[BrowserAPI.Prefix('transformOrigin')] := '50% 50%';
    FContent.Handle.style[BrowserAPI.Prefix('Transform')] := 'translateZ(0px)';

Oh and it fades out the indicator after a scroll session, quite nice if I say so myself 🙂

Enjoy!

unit Form1;

interface

uses
  System.types, System.Colors,
  System.Events, System.Time, System.Widget, System.Objects,

  W3C.Date, W3C.DOM,

  SmartCL.Effects,

  SmartCL.Events, SmartCL.MouseCapture, SmartCL.System, SmartCL.Graphics,
  SmartCL.Components, SmartCL.Forms,  SmartCL.Fonts, SmartCL.Borders,
  SmartCL.Application, SmartCL.Controls.Listbox, SmartCL.Controls.Panel,
  SmartCL.Controls.CheckBox, SmartCL.Controls.Button;

type

  TScrollContent = class(TW3CustomControl)
  end;

  TW3ScrollIndicator = class(TW3CustomControl)
  end;

  TW3VScrollControl = class(TW3CustomControl)
  private
    FYOffset: integer;
    FContent: TScrollContent;
    FVRange:  TW3Range;
    FHRange:  TW3Range;
    FPressed: boolean;
    FStartY:  integer;

    FTarget: integer;
    FAmplitude: double;
    FTimestamp: integer;
    FVelocity: double;
    FFrame: double;
    FTicker: TW3DispatchHandle;
    FFader: TW3DispatchHandle;
    FTimeConstant: double;

    FMouseDownEvent: TW3DOMEvent;
    FMouseUpEvent: TW3DOMEvent;
    FMouseMoveEvent: TW3DOMEvent;
    FTouchDownEvent: TW3DOMEvent;
    FTouchMoveEvent: TW3DOMEvent;
    FTouchEndsEvent: TW3DOMEvent;

    FIndicator: TW3ScrollIndicator;
    function  GetYPosition(const E: variant): integer;
    procedure MoveBegins(sender: TObject; EventObj: JEvent);
    procedure MoveEnds(sender: TObject; EventObj: JEvent);
    procedure MoveUpdate(sender: TObject; EventObj: JEvent);
    procedure HandleContentSizeChanged(sender: TObject);
  protected
    procedure Track;virtual;
    procedure AutoScroll;virtual;

    procedure ScrollBegins;virtual;
    procedure ScrollEnds;virtual;

    procedure Resize;override;
    procedure InitializeObject; override;
    procedure FinalizeObject; override;
    procedure ObjectReady;override;
    procedure ScrollY(const NewTop: integer);
  public
    Property  Content:TScrollContent read FContent;
  end;

  TForm1 = class(TW3Form)
    procedure W3Button1Click(Sender: TObject);
  private
    {$I "Form1:intf"}
    FBox: TW3VScrollControl;
  protected
    procedure InitializeForm; override;
    procedure InitializeObject; override;
    procedure Resize; override;
  end;

implementation

//###################################################################
// TW3VScrollControl
//###################################################################

procedure TW3VScrollControl.InitializeObject;
begin
  inherited;
  FPressed:=false;
  FYOffset := 0;
  FStartY := 0;

  FTimeConstant := 325;

  Background.fromColor(clWhite);
  FContent := TScrollContent.Create(self);
  FIndicator:=TW3ScrollIndicator.Create(self);
  FIndicator.width:=8;
  FIndicator.height:=32;
  FIndicator.StyleClass:='TW3ScrollContentIndicator';
  FIndicator.Transparent := true;

  FMouseDownEvent := TW3DOMEvent.Create(self);
  FMouseDownEvent.Attach("mousedown");
  FMouseDownEvent.OnEvent := @MoveBegins;

  FMouseMoveEvent := TW3DOMEvent.Create(self);
  FMouseMoveEvent.Attach("mousemove");
  FMouseMoveEvent.OnEvent := @MoveUpdate;

  FMouseUpEvent := TW3DOMEvent.Create(self);
  FMouseUpEvent.Attach("mouseup");
  FMouseUpEvent.OnEvent := @MoveEnds;

  FTouchDownEvent := TW3DOMEvent.Create(self);
  FTouchDownEvent.Attach("touchstart");
  FTouchDownEvent.OnEvent:= @MoveBegins;

  FTouchMoveEvent := TW3DOMEvent.Create(self);
  FTouchMoveEvent.Attach("touchmove");
  FTouchMoveEvent.OnEvent := @MoveUpdate;

  FTouchEndsEvent := TW3DOMEvent.Create(self);
  FTouchEndsEvent.Attach("touchend");
  FTouchEndsEvent.OnEvent := @MoveEnds;

  FContent.Handle.ReadyExecute(
  procedure ()
  begin
    (* Mark content for GPU acceleration *)
    FContent.SetInitialTransformationStyles;
  end);
end;

procedure TW3VScrollControl.ObjectReady;
begin
  inherited;
  FContent.OnReSize := HandleContentSizeChanged;
  FIndicator.left:=ClientWidth-FIndicator.width;
  FIndicator.bringToFront;
  FIndicator.Visible:=false;
  resize;
end;

procedure TW3VScrollControl.FinalizeObject;
begin
  FContent.free;
  inherited;
end;

procedure TW3VScrollControl.HandleContentSizeChanged(sender: TObject);
begin
  if not (csDestroying in ComponentState) then
  begin
    FVRange := TW3Range.Create(0, FContent.Height - ClientHeight);
    FHRange := TW3Range.Create(0, FContent.Width - ClientWidth);
  end;
end;

procedure TW3VScrollControl.Resize;
var
  LClient:  TRect;
begin
  inherited;
  if (csReady in ComponentState) then
  begin
    LClient := ClientRect;
    FVRange := TW3Range.Create(0, FContent.Height - LClient.Height);
    FHRange := TW3Range.Create(0, FContent.Width - LClient.Width);
    FContent.SetBounds(0,FContent.top,LClient.Width,FContent.height);
    FIndicator.MoveTo(ClientWidth-FIndicator.Width,FIndicator.top);
  end;
end;

procedure TW3VScrollControl.ScrollY(const NewTop: integer);
var
  LGPU: string;
  LIndicatorTarget: integer;

  function GetRelativePos:double;
  begin
    result := (ClientHeight - FIndicator.Height) / (FContent.Height - ClientHeight);
  end;

begin
  if not (csDestroying in ComponentState) then
  begin
    if (csReady in ComponentState) then
    begin
      (* Use GPU scrolling to position the content *)
      FYOffset := FVRange.ClipTo(NewTop);
      LGPU := "translate3d(0px,";
      LGPU += FloatToStr(-FYOffset) + "px, 0px)";
      FContent.Handle.style[BrowserAPI.Prefix("Transform")] := LGPU;

      (* Use GPU scrolling to position the indicator *)
      LIndicatorTarget := FYOffset * GetRelativePos;
      FIndicator.left := clientwidth - FIndicator.width;
      LGPU :="translateY(" + TInteger.ToPxStr(LIndicatorTarget) + ")";
      FIndicator.Handle.style[BrowserAPI.Prefix("Transform")]:= LGPU;
    end;
  end;
end;

procedure TW3VScrollControl.Track;
var
  LNow: integer;
  Elapsed: integer;
  Delta: double;
  V: double;
begin
  LNow := TW3Dispatch.JsNow.now();
  Elapsed := LNow - FTimestamp;
  FTimestamp := TW3Dispatch.JsNow.now();
  Delta := FYOffset - FFrame;
  FFrame := FYOffset;
  v := 1000 * Delta / (1 + Elapsed);
  FVelocity := 0.8 * v + 0.2 * FVelocity;
end;

procedure TW3VScrollControl.ScrollBegins;
begin
  TW3Dispatch.ClearInterval(FFader);
  if not (csDestroying in ComponentState) then
  begin
    FIndicator.Visible := true;
    FIndicator.AlphaBlend := true;
    FIndicator.Opacity := 255;
  end;
end;

procedure TW3VScrollControl.ScrollEnds;
begin
  TW3Dispatch.ClearInterval(FFader);
  if not (csDestroying in ComponentState) then
  begin
    FFader:=TW3Dispatch.SetInterval(procedure ()
      begin
        FIndicator.AlphaBlend := true;
        FIndicator.Opacity := FIndicator.Opacity - 10;
        if FIndicator.Opacity=0 then
        begin
          TW3Dispatch.ClearInterval(FFader);
        end;
      end,
      50);
  end;
end;

procedure TW3VScrollControl.AutoScroll;
var
  Elapsed: integer;
  Delta: double;
begin
  if FAmplitude<>0 then
  begin
    Elapsed := TW3Dispatch.JsNow.now() - FTimestamp;
    Delta := -FAmplitude * Exp(-Elapsed / FTimeConstant);
  end;

  (* Scrolled passed end-of-document ? *)
  if (FYOffset >= (FContent.Height - ClientHeight)) then
  begin
    TW3Dispatch.ClearInterval(FTicker);
    FTicker := unassigned;
    ScrollY(FContent.Height-ClientHeight);
    ScrollEnds;
    exit;
  end;

  (* Scrolling breaches beginning of document? *)
  if (FYOffset < 0) then   begin     TW3Dispatch.ClearInterval(FTicker);     FTicker := unassigned;     ScrollY(0);     ScrollEnds;     exit;   end;   if (delta > 5) or (delta < -5) then   begin     ScrollY(FTarget + Delta);     W3_RequestAnimationFrame(AutoScroll);   end else   begin     ScrollY(FTarget);     ScrollEnds;   end; end; function TW3VScrollControl.GetYPosition(const e: variant): integer; begin   if ( (e.targetTouches) and (e.targetTouches.length >0)) then
  result := e.targetTouches[0].clientY else
  result := e.clientY;
end;

procedure TW3VScrollControl.MoveBegins(sender: TObject; EventObj: JEvent);
begin
  FPressed := true;
  FStartY := GetYPosition(EventObj);
  FVelocity := 0;
  FAmplitude := 0;
  FFrame := FYOffset;
  FTimestamp := TW3Dispatch.JsNow.now();
  TW3Dispatch.ClearInterval(FTicker);
  FTicker := TW3Dispatch.SetInterval(Track,100);
  EventObj.preventDefault();
  EventObj.stopPropagation();
end;

procedure TW3VScrollControl.MoveUpdate(sender: TObject; EventObj: JEvent);
var
  y, delta: integer;
begin
  if FPressed then
  begin
    y := GetYPosition(eventObj);
    delta := (FStartY - Y);
    if (Delta>2) or (Delta < -2) then     begin       FStartY := Y;       ScrollY(FYOffset + Delta);     end;   end;   EventObj.preventDefault();   EventObj.stopPropagation(); end; procedure TW3VScrollControl.MoveEnds(sender: TObject; EventObj: JEvent); begin   FPressed := false;   TW3Dispatch.ClearInterval(FTicker);   if (FVelocity > 10) or (FVelocity < -10) then
  begin
    FAmplitude := 0.8 * FVelocity;
    FTarget := round(FYOffset + FAmplitude);
    FTimeStamp := TW3Dispatch.JsNow.Now();

    ScrollBegins;
    w3_requestAnimationFrame(autoscroll);
  end;
  EventObj.preventDefault();
  EventObj.stopPropagation();
end;

{ TForm1 }

procedure TForm1.W3Button1Click(Sender: TObject);
begin
  self.FBox.Content.height:=1000;
end;

procedure TForm1.InitializeForm;
begin
  inherited;

  // this is a good place to initialize components
  FBox := TW3VScrollControl.Create(self);
  FBox.SetBounds(10,10,300,300);

  //

  var LText :="
<table cellpadding=|0px| style=|border-collapse: collapse| width=|100%|>";
  for var x:=1 to 400 do
  begin
    if ((x div 2) * 2) = x then
    LText += "
<tr padding=|0px| style=|border: 0px solid black; background:#ECECEC|>" else
    LText += "
<tr style=|border: 0px solid black; background:#FFFFFF|>";
    LText += "
<td padding=|0px| height=|32px| style=|border-bottom: 1px solid #ddd|>" + x.toString + "</td>
";
    LText += "
<td style=|border-bottom: 1px solid #ddd|>List item #" + x.toString + "</td>
";
    LText += "</tr>
";
  end;
  LText +="</table>
";
  LText := StrReplace(LText,'|','''');

  FBox.Content.innerHTML := LText;
  FBox.Content.width:=1000;
  FBox.Content.height := FBox.Content.ScrollInfo.ScrollHeight;

end;

procedure TForm1.InitializeObject;
begin
  inherited;
  {$I "Form1:impl"}
end;

procedure TForm1.Resize;
begin
  inherited;
  if (csReady in ComponentState) then
  begin
    //FBox.setBounds(10,10,clientwidth div 2, clientHeight div 2);
  end;
end;

initialization
begin
  Forms.RegisterForm({$I %FILE%}, TForm1);
end;

end.

Smart Data, working on it!

May 24, 2016 4 comments

Right! Its been two hell of a couple of weeks. I’m multi-tasking as best I can between:

  • Re-writing large parts of the VJL
  • Writing a book
  • Talking to key figures in the community
  • Presenting ideas and meeting with the Smart Pascal Consortium
  • Yoga and inner peace
  • Kids, GF and a snapping 3-level spinal injury
  • Not in that order

Whats on the table?

In short, a quantum leap. We have the technology but it’s all buried in unit files and the lack of documentation is just “enough”. So I have gotten to the stage where I will just deal with it come hell or high water.

I have had a little debate with David Berneda, owner and all-round super-coder over at Steema software about databases. He knows data since his components run on several platforms, under many different languages — so he was indeed the guy to talk to and get some inspiration.

The problem with data is threefold:

  • The pascal side of things
  • How data is dealt with in the JS community in general
  • Traditional access to data via nodeJS and native drivers

In effect Smart Mobile Studio needs to have a system that covers all these aspects. The pascal code (regardless of what is wrapped or underneath) should be, well, pascal. It should be directly readable, infinitely intuitive for Lazarus and Delphi programmers – but at the same time flexible and expose all the dynamic features JavaScript has (that Delphi doesnt).

A kid made $4 million on a flashlight app, a blank white screen written in Javascript. When Delphi finally got to Appstore, it was all over.

And no, I’m not out to get Delphi; A bit pissed at EMB for screwing over the community on iOS and Android (not to mention the pricing which is just ridicules), but still a loyal Spartan (!).

What I am trying to do, and in fact the whole SMS team, is to get your native Delphi products into places where Embarcadero cannot reach. To compliment Delphi and rejuvinate your skill-set which is being phased out due to the mistakes of EMB (and they did inherit a fair bit of mistakes from Borland so its not all their fault).

And JSVM is not on their to-do list, they are strictly native. Same with Remobjects. So SMS is the only one surfing on the JavaScript virtual machine.

Speaking of data

Then you have inter-operative functionality. Namely that you want your SMS applications talking to off-the-shelves JS solutions out there. If the data in SMS is to “Delphi” and we have to do conversion just to get Linq.js to chew it, then we have lost something.

This is what Linq (from C#) looks like under JavaScript (we can do better!). You can use Linq with SMS today, its actually childs-play to wrap it in a unit.

Linq.JS -- 1:1 identical with C# and the dot net framework -- and you can use this today in smart if you like

Linq.JS — 1:1 identical with C# and the dot net framework

Just look at that mess. Seriously, we can do better and make it worthy object pascal.

I want SMS programmers to just go “damn! this rocks! Now I can get my Delphi data running on my website, my phone, my embedded devices and whatever IOT comes next” without having to wait 2-3 years for Embarcadero to write yet-another-compiler.

Do you know why im so pissed at Embarcadero? Because they cheated us not once, but twice! First with the iPhone and then Android. By the time we got to code for these platforms the market was saturated. Every possible application was done and every concivable window of opportunity you may have had dead.

A kid made $4 million on a flashlight app, a blank white screen written in Javascript. When Delphi finally got to Appstore, it was all over.

And they did this not once, but twice! 

We just can’t have an infrastructure or programming language that takes 2 blody years to pivot. It’s just not acceptable. Freepascal was able to support iOS in what, 3 weeks? And even with stealing their codebase it took Embarcadero 2 years? And people ask “why JavaScript”. It should be considered a global intellectual emergency that a company can charge these prices.

Dude, Javascript has what — 20.000 commiters? Millions of active users and the amount of coders contributing to the runtime ranks in the tens of thousands (if not more). The V8 LLVM powered JSVM has seen more commits and improvements than Microsoft Windows. It is being payed for by Google, Apple and a whole range of Linux companies. JS is able to pivot on a dime and target a piece of hardware in less than an hour. Especially with the LLVM sub-strata. I also noticed a fork by Apple using Clang, so native languages and the desktop has an expiration date. Just saying..

Do you think we picked JSVM just for fun?

You have missed the rise of the full stack JavaScript developer, and its happening right now, while the desktop is being marginalized at rapid pace.

A whole new world

Did you know that JavaScript ranks #8 on the list of used languages in the world? Did you know that 50% (fifty percent!) of all the programs being written right now on this planet, is written in JavaScript? Just let that sink in for a moment.

There is a whole reality of IOT and business that Delphi developers are missing. Jobs thin on the ground? No. Not really. But if you limit yourself to only delivering pascal projects then sure. Add JavaScript to the list and see what happens.

This is a podcast I find quite interesting: https://javascriptair.com/

Any-ho, then, just when things are looking cosy and within grasp data-side, nodeJS and IO goes and adds support for native database plugins. You know, stuff that pretty much looks like what we already have in Lazarus and Delphi (although slightly perverted being javascript after all).

“Fine, I only have one bullet so your gonna have to share!”
-Source: Deadpool, the movie

For a taste of how RAW NodeJS DB connectivity looks like, have a taste of this. Not really elegant is it? So im going to make that puppy into easy to use, sexy, slick and (drumroll) object pascal style components.

Now, I'm about to do to you what Limp Bizkit did to music in the late 90s.

Now, I’m about to do to you what Limp Bizkit did to music in the late 90s.

I’m thinking MSSQL, MySQL, MariaDB and Oracle should be more than enough to get you started (server-side). We already have Remobjects SDK support and Datasnap, Websocket.. that should about cover it (when the docs are done).

mORMotsupport, finally!

Got in touch with Arnaud Bouchez over at Synopse, and we will finally add built-in support for mORMot. No time-table on that yet, but ill keep you posted as we move along. Arnaud has some great code and I am really looking forward to getting the IDE talking with their framework!

Also super tempted to have a stab at compiling his Spidermonkey fork of the Mozilla Javascript virtual machine, on ARM to see if we can get that linked and running under freepascal on the PI3. That would be awesome.

Non visual components

Yes, it’s finally in the RTL. Again, no time-table on the IDE changes but it’s now an actual part of the inheritance chain. TW3Widget is now the ancestor of TW3TagObj, so we keep the SmartCL namespace (document object model) away from the non-visual ancestor for now. But it’s there and working!

TAction and TActionManager

Yup. Done, tested and very sexy! Again no time-table on when we get this into the IDE, but from the VJL point of view you can now create actions and bind that to controls that supports it (in the traditional 1 action – many subscribers). That is something i have missed!

Tweening and software animation

We already have GPU animations and control under wraps, thats been there for quite some time now (smartcl.effects.pas), but tweening gives you better control and for desktop applications it can be a better option. And it’s there. Hopefully we can get a visual aid for it, although an editor for tweening is overkill.. really. But if it makes it easier to use, so be it.

A tweening prototype you can play with straight away can be found here (its more evolved in the RTL naturally).

Is there more?

Yes. But thats hush-hush stuff so I cant talk about it.

 

 

Partial classes in Smart Pascal, how?

May 19, 2016 Leave a comment
Fast, binary, pure object pascal

Partial classes

Ralf Wesseling mentioned something very important to me today, namely that since partial classes is not in Delphi – getting to know this under Smart Pascal, which sadly is seriously lacking in the documentation department, is a bit of a sour apple.

So to rectify this I hope this little document will help.

What is partial classes?

A partial class is, as the name hints to, a class that is only implemented in part. Depending on the language (and even dialect), partial classes can have many aspects. But in essence it comes down to:

  • You can define parts of a class spread out over several units
  • The class is not sealed (or considered complete) until the churned through everything

Ok — so let’s define a class and see what it looks like:


unit firstunit;

interface

uses
  SmartCL.System, SmartCL.Graphics, SmartCL.Components, SmartCL.Forms,
  SmartCL.Fonts, SmartCL.Borders, SmartCL.Application;

type

  // Our own new partial class !
  TMyButton = partial class(TW3Button)
  public
    procedure Testmethod;
  end;

implementation

procedure TMyButton.Testmethod;
begin
end;

end.
 

Nothing to fancy there, in fact, what is the difference?

The difference is that you can go into another unit, one that links to the unit we have our above code in, and then continue to implement it there:

unit secondunit;

interface

uses
  firstunit,
  SmartCL.System, SmartCL.Graphics, SmartCL.Components, SmartCL.Forms,
  SmartCL.Fonts, SmartCL.Borders, SmartCL.Application;

type

  // Our own new partial class !
  TMyButton = partial class(TW3Button)
  public
    procedure Testmethod2;
  end;

implementation

procedure TMyButton.Testmethod2;
begin
end;

end.

Normally this would not be allowed. Had we been using non-partial classes, the class in second unit would have to inherit from TMyButton in order to extend it.

What the compiler does when it encounter a partial class, is that it waits until all the units have been parsed and then it collects all the pieces and puts together “the final virtual method table” for that class.

Cool side effects

Start a new visual smart mobile studio project. Just a plain visual project.

Now go to the uses clause and add: “SmartCL.effects” to the list of units.

Now go down to InitializeForm, and write

self.

The code proposal window should pop-up: Notice how suddenly ALL TW3MovableControl based components have suddenly gained 20+ “fx” special effects methods 🙂

  TW3MovableControl = partial class(TW3Component)
    function  fxFadeOut(const Duration:Float):TW3MovableControl;overload;
    Procedure fxFadeOut(const Duration:Float;
              const OnFinished:TProcedureRef);overload;

    function  fxFadeIn(const Duration:Float):TW3MovableControl;overload;
    Procedure fxFadeIn(const Duration:Float;
              const OnFinished:TProcedureRef);overload;

    function  fxWarpOut(const Duration:Float):TW3MovableControl;overload;
    Procedure fxWarpOut(const Duration:Float;
              const OnFinished:TProcedureRef);overload;

    function  fxWarpIn(const Duration:Float):TW3MovableControl;overload;
    procedure fxWarpIn(const Duration:Float;
              const OnFinished:TProcedureRef);overload;

    function  fxZoomIn(const Duration:Float):TW3MovableControl;overload;
    Procedure fxZoomIn(const Duration:Float;
              const OnFinished:TProcedureRef);overload;

    function  fxZoomOut(const Duration:Float):TW3MovableControl;overload;
    Procedure fxZoomOut(const Duration:Float;
              const OnFinished:TProcedureRef);overload;

    function  fxScaleTo(const aToX,aToY,aToWidth,aToHeight:Integer;
              const Duration:Float):TW3MovableControl;overload;
    Procedure fxScaleTo(const aToX,aToY,aToWidth,aToHeight:Integer;
              const Duration:Float;
              const OnFinished:TProcedureRef);overload;

    function  fxMoveTo(const dx,dy:Integer;
              const Duration:Float):TW3MovableControl;overload;
    Procedure fxMoveTo(const dx,dy:Integer;
              const Duration:Float;
              const OnFinished:TProcedureRef);overload;

    function  fxMoveBy(const dx,dy:Integer;
              const Duration:Float):TW3MovableControl;overload;
    Procedure fxMoveBy(const dx,dy:Integer;
              const Duration:Float;
              const OnFinished:TProcedureRef);overload;

    function  fxMoveUp(const Duration:Float):TW3MovableControl;overload;
    Procedure fxMoveUp(const Duration:Float;
              const OnFinished:TProcedureRef);overload;

    function  fxMoveDown(const Duration:Float):TW3MovableControl;overload;
    procedure fxMoveDown(const Duration:Float;
              const OnFinished:TProcedureRef);overload;

    function  fxSizeTo(const aWidth,aHeight:Integer;
              const Duration:Float):TW3MovableControl;overload;
    Procedure fxSizeTo(const aWidth,aHeight:Integer;
              const Duration:Float;
              const OnFinished:TProcedureRef);overload;

    function fxScaleDown(aFactor:Integer;
              const Duration:Float):TW3MovableControl;overload;
    procedure fxScaleDown(aFactor:Integer;const Duration:Float;
              const OnFinished:TProcedureRef);overload;

    function  fxScaleUp(aFactor:Integer;
              const Duration:Float):TW3MovableControl;overload;
    Procedure fxScaleUp(aFactor:Integer;const Duration:Float;
              const OnFinished:TProcedureRef);overload;

    function  fxBusy:Boolean;
    Procedure fxSetBusy(const aValue:Boolean);
  End;

This is the magic of partial classes. Without the SmartCL.effects.pas unit, they were just boring old controls. But since TW3MovableControl is marked as partial, the effects unit can extend the class with all that new cool stuff.

Well, that was a crash course in partial classes — hope it clears things up. There is more to it than this ofcourse, but thats an immediate feature you can start using straight away 🙂

 

Smart Mobile Studio Unleashed, book

May 1, 2016 3 comments
cover

Embedded? Cloud? Oh i’ll give you something to play with alright

Spent the better 3 weeks writing a book, a book that should have been written ages ago but I never had the time to do so.

What will it cover? Well, let’s just say this is the book you want for all things esoteric and secret about Smart Mobile Studio. It is not a book for beginners in object pascal, so a background in Delphi or Freepascal is a pre-requisite.

I am presently at 64 A4 pages packed full of stuff, and that’s not even half of the curriculum defined. We go through the inheritance chain for visual controls, from TW3TagObj all the way up to the culmination in TW3CustomControl.

We cover esoteric topics like talking to the GPU for blistering fast graphics, how to effectively use the class helpers attached to each control handle, memory management, font handling, enumerating child elements properly (read: why JavaScript runs faster the more you fragment) — and of-course, all my secrets in writing fast displays, like TW3DataGrid (not yet published) that can cut through thousands of rows without breaking a sweat.

But fear not, I also over fundamental concepts like how the VJL is organized, the namespaces, the platforms we support — and more importantly, WHY partial classes plays such an important part of it all.

But the juicy parts that’s gonna make you get all girly inside are in the NodeJS chapters, how to write clustering and make your NodeJS services scale beyond anything delivered by Delphi or Freepascal to date.

I am co-authoring the book with my good friend Peter Dunne of Jazenga Software who is taking my second-language-english and making it more natural to english speaking natives.

What can only be called “the black book of smart pascal sorcery” will be available through LuLu publishing later this year. It will co-incide with a major shift in the VJL.

You’ll be running rings around expensive native cloud services quite soon.

Here are a few teasers just to wet your appetite. These are fresh off the mint so to speak, so not even spellchecked yet. But should be enough to get your creative side going 🙂

snapshot_01snapshot_02snapshot_03snapshot_04

plexi

Writing a web front-end for your NAS or nodeJS embedded service is a piece of cake!

 

 

Smart Mobile libs on GitHub

December 29, 2015 Leave a comment

I’ve been running this blog for a few years now, and being a hard-core SVN user I have been forced to just publish code in the article itself. Well, I decided it was time to change all this, so I’ve setup a spanking new Github repository just to make things easier.

So point your Github desktop at: https://github.com/quartexNOR/smartpascal for Smart Mobile Studio units and code.

If you browse my repositories you will find several libraries, so feel free to browse around. ByteRage and PixelRage should be of special interest to coders looking for things your normal streams and bitmaps just cant deliver 🙂

Tweening, an update

I got plenty of cool feedback on the tweening unit, so that is the first unit to be uploaded to GIT. And as you probably understand, after work today i sat down and expanded the original class. Monumentally so.

The new tweening system is not limited to a single value, in fact, you can tween as many values as you want from the same object.

You also have plenty of events, both per tween element and for all active tweens as a group. This is handy if you have an animation which affects multiple objects, but should be considered “a single animation” or “sequence”.

You can also pause a single tween element, a list of elements or all elements. You can likewise resume with the same level of detail.

Better timing

The new TTween class provides two ways of updating your tween-elements. The first is a normal timer object (TTimer) which update on interval. This is best suited for non-critical UI animations.

The second is more suitable for games and multimedia, and runs the tweens within a synchronized animation frame loop (requestAnimationFrame API). When using this your updates are performed in sync with the redraw cycle of the browser.

Better behavior

A tween now has 3 modes of execution, these are:

  • tbSingle
  • tbRepeat
  • tbOscillate

The last option executes your tween, then executes it backwards, and then continues like this in an oscillating fashion (hence the name). This sort of behavior means that an animation may never actually end (!)

To solve this I introduced a new property called “IgnoreOscillate”. If this property is set to false, the TTween object will stop when all tweens have been executed as expected. The oscillating behavior of some Tween elements will simply be ignored.

If you set the property to True, TTween will never exit the animation loop – but will instead issue the new OnPartialFinished event. This event tells you that all tweens are done, but that some tween elements are still running in oscillating mode (read: so it’s safe to call cancel() to stop everything).

Individual events

Since tween data is now isolated in separate objects, each tween have personal events (so to speak). The TTweenElement class exposes OnFinished and OnUpdated events.

TTween also issues a OnUpdated event after all TTweenElements have been updated. So depending on the type of animation you are executing, you have the freedom to chose both individual notification and “global” notification.

The source

For those that dont have access to GIT right now, here are the changes

unit system.animation.tween;

interface

uses 
  System.Types,
  SmartCL.Time,
  SmartCL.System;

type

  TW3TweenAnimationType = (
    ttlinear,
    ttQuadIn,
    ttQuadOut,
    ttQuadInOut,
    ttCubeIn,
    ttCubeOut,
    ttCubeInOut,
    ttQuartIn,
    ttQuartOut,
    ttQuartInOut
    );

  TTweenBehavior = (
    tbSingle,         // Execute once and then stops
    tbRepeat,         // Repeats the tween sequence
    tbOscillate      // Executes between A and B in oscillating manner
    );

  TTweenData    = class;
  TTweenElement = class;
  TW3TweenEase  = class;
  TW3Tween      = class;

  TW3TweenEase = class
  public
    class function  Linear(t,b,c,d:float):float;
    class function  QuadIn(t, b, c, d:float):float;
    class function  QuadOut(t, b, c, d:float):float;
    class function  QuadInOut(t, b, c, d:float):float;
    class function  CubeIn(t, b, c, d:float):float;
    class function  CubeOut(t, b, c, d:float):float;
    class function  CubeInOut(t, b, c, d:float):float;
    class function  QuartIn(t, b, c, d:float):float;
    class function  QuartOut(t, b, c, d:float):float;
    class function  QuartInOut(t, b, c, d:float):float;
  end;

  TW3TweenItemUpdatedEvent = procedure (Sender:TObject;Item:TTweenElement);
  TW3TweenUpdatedEvent = procedure (Sender:TObject);
  TW3TweenStartedEvent = procedure (sender:TObject);
  TW3TweenFinishedEvent = procedure (sender:TObject);
  TW3TweenFinishedPartialEvent = procedure (sender:TObject);

  TTweenState = (tsIdle,tsRunning,tsPaused,tsDone);

  TTweenData = class
  public
    Property    Id: String;
    Property    StartTime: TDateTime;
    Property    StartValue: Float;
    Property    Distance: Float;
    Property    Duration: Float;
    Property    AnimationType: TW3TweenAnimationType;
    Property    Behavior: TTweenBehavior;

    function    Expired: Boolean;virtual;
    Procedure   Reset;virtual;
  end;

  TTweenElement = class(TTweenData)
  public
    Property    State:TTweenState;
    Property    Value:Float;

    Property    OnFinished:TNotifyEvent;
    Property    OnUpdated:TW3TweenItemUpdatedEvent;

    Procedure   Reset;override;
    Constructor Create;virtual;
  end;

  TW3Tween = class
  private
    FTimer:     TW3Timer;
    FValues:    Array of TTweenElement;
    FActive:    Boolean;
    FPartial:   Boolean;
    FInterval:  Integer;
  protected
    Procedure   HandleSyncUpdate;virtual;
    procedure   HandleUpdateTimer(Sender:TObject);virtual;
    function    Update(const Item:TTweenElement):Float;virtual;
  public

    function    ObjectOf(const Id:String):TTweenElement;
    function    IndexOf(Id:String):Integer;

    Property    Active:Boolean read ( FActive );
    Property    Item[const index:Integer]:TTweenElement read (FValues[index]);
    property    Tween[const Id:String]:TTweenElement read ObjectOf;
    property    Count:Integer read ( FValues.Length );
    Property    Interval:Integer read FInterval write ( TInteger.EnsureRange(Value,1,10000) );

    Property    SyncRefresh:Boolean;
    Property    IgnoreOscillate:Boolean;

    function    Add(Id:String):TTweenElement;overload;

    Function    Add(Id:String;const aStartValue,aDistance,aDuration:float;
                const aAnimationType:TW3TweenAnimationType;
                const aBehavior:TTweenBehavior):TTweenElement;overload;

    Procedure   Delete(index:Integer);overload;
    procedure   Delete(Id:String);overload;
    procedure   Delete(const TweenIds:Array of String);overload;

    procedure   Clear;overload;

    procedure   Execute;overload;
    procedure   Execute(const TweenObjects:Array of TTweenElement);overload;

    Procedure   Pause(const Index:Integer);overload;
    procedure   Pause(const Tween:TTweenElement);overload;
    procedure   Pause(const Objs:Array of TTweenElement);overload;
    procedure   Pause(const Ids:Array of String);overload;
    procedure   Pause;overload;

    Procedure   Resume(const index:Integer);overload;
    procedure   Resume(const Tween:TTweenElement);overload;
    procedure   Resume(const Objs:Array of TTweenElement);overload;
    procedure   Resume(const Ids:Array of String);overload;
    procedure   Resume;overload;

    procedure   Cancel;overload;

    Constructor Create;virtual;
    Destructor  Destroy;Override;

  published
    Property    OnPartialFinished:TW3TweenFinishedPartialEvent;
    Property    OnUpdated:TW3TweenUpdatedEvent;
    Property    OnFinished:TW3TweenFinishedEvent;
    Property    OnStarted:TW3TweenStartedEvent;
  end;


function GetTimeCode:float;
function Round100(const Value:float):float;

implementation

Function GetTimeCode:float;
begin
  asm
    @result = Date.now();
  end;
end;

function Round100(const Value:float):float;
begin
  result := Round( Value * 100) / 100;
end;

//############################################################################
// TW3Tween
//############################################################################

Constructor TW3Tween.Create;
begin
  inherited Create;
  FTimer := TW3Timer.Create;
  Interval := 10;
  IgnoreOscillate := true;
end;

Destructor  TW3Tween.Destroy;
begin
  Cancel;
  Clear;
  FTimer.free;
  inherited;
end;

procedure TW3Tween.Clear;
begin
  While FValues.length>0 do
  begin
    FValues[FValues.length-1].free;
    Fvalues.Delete(FValues.length-1,1);
  end;
end;

function TW3Tween.Update(const Item:TTweenElement):Float;
var
  LTotal: float;

  function PerformX(t, b, c, d:float):float;
  begin
    result := 0.0;
    case Item.AnimationType of
    ttlinear:     result := TW3TweenEase.Linear(t,b,c,d);
    ttQuadIn:     result := TW3TweenEase.QuadIn(t,b,c,d);
    ttQuadOut:    result := TW3TweenEase.QuadOut(t,b,c,d);
    ttQuadInOut:  result := TW3TweenEase.QuadInOut(t,b,c,d);
    ttCubeIn:     result := TW3TweenEase.CubeIn(t,b,c,d);
    ttCubeOut:    result := TW3TweenEase.CubeOut(t,b,c,d);
    ttCubeInOut:  result := TW3TweenEase.CubeInOut(t,b,c,d);
    ttQuartIn:    result := TW3TweenEase.QuartIn(t,b,c,d);
    ttQuartOut:   result := TW3TweenEase.QuartOut(t,b,c,d);
    ttQuartInOut: result := TW3TweenEase.QuartInOut(t,b,c,d);
    end;
  end;

begin
  if not Item.Expired then
  begin
    LTotal := PerformX(GetTimeCode-Item.StartTime,
                      Item.StartValue,
                      Item.Distance,
                      Item.Duration);
  end else
  if Item.behavior = tbSingle then
  begin
    LTotal := Item.StartValue + Item.Distance;
  end else
  if Item.behavior = tbRepeat then
  begin
    Item.StartTime := GetTimeCode;
    LTotal := PerformX(GetTimeCode-Item.StartTime,
                      Item.StartValue,
                      Item.Distance,
                      Item.Duration);
  end else
  if Item.behavior = tbOscillate then
  begin
    Item.StartValue := Item.StartValue + Item.Distance;
    Item.Distance := -Item.Distance;
    Item.StartTime := GetTimeCode;
    LTotal := PerformX(GetTimeCode-Item.StartTime,
                      Item.StartValue,
                      Item.Distance,
                      Item.Duration);
    Item.State := tsDone;
  end;

  result := Round100(LTotal);
end;

procedure TW3Tween.HandleUpdateTimer(Sender:TObject);
var
  LItem:  TTweenElement;
  LCount: Integer;
  LDone:  Integer;
begin
  (* Tween objects cleared while active? *)
  if FValues.Length<1 then
  begin
    Cancel;
    exit;
  end;

  LCount := 0;
  LDone  := 0;
  for LItem in FValues do
  begin
    (* The start time-code is set in the first update *)
    if LItem.State=tsIdle then
    begin
      LItem.StartTime := GetTimeCode;
      LItem.State := tsRunning;
    end;

    // Animation paused? Continue with next
    if LItem.State = tsPaused then
    continue;

    // Expired? Keep track of it
    if LItem.Expired then
    begin
      if IgnoreOscillate then
      begin
        if (LItem.Behavior <> tbOscillate) then
        inc(LCount) else
        inc(LDone);
      end else
      inc(LCount);
    end;

    LItem.Value := Update(LItem);

    // Fire "per item" event
    if assigned(LItem.OnUpdated) then
    LItem.OnUpdated(self,LItem);

    // finished on this run?
    if LItem.Expired then
    begin
      if not Litem.State = tsDone then
      begin
        LItem.State := tsDone;
        if assigned(LItem.OnFinished) then
        LItem.OnFinished(LItem);
      end;
    end;

  end;

  if assigned(OnUpdated) then
  OnUpdated(Self);

  if LCount = (FValues.Length - LDone) then
  begin
    if IgnoreOscillate then
    begin
      if not FPartial then
      begin
        FPartial := True; // make sure this happens only once!
        if assigned(OnPartialFinished) then
        OnPartialFinished(self);
      end;
    end;
  end;

  (* If all tweens have expired, stop the animation *)
  If LCount = FValues.Length then
  begin
    //Writeln('Stopping at ' + LCount.toString + ' tweens done');
    Cancel;
  end;
end;

Procedure TW3Tween.HandleSyncUpdate;
begin
  HandleUpdateTimer(NIL);
  if Active then
  W3_RequestAnimationFrame(HandleSyncUpdate);
end;

procedure TW3Tween.Execute(Const TweenObjects:Array of TTweenElement);
var
  LItem:  TTweenElement;
  LId:    String;
begin
  if not Active then
  begin
    if TweenObjects.length<0 then
    begin
      for LItem in TweenObjects do
      begin
        LId := LItem.Id.Trim;
        if LId.length>0 then
        begin

          if self.IndexOf(LId)<0 then
          FValues.add(LItem) else
          Raise EW3Exception.CreateFmt
          ('Execute failed, a tween-object with id [%s] already exists in collection error',[LId]);
        end else
        Raise EW3Exception.Create
        ('Execute failed, could not inject tween object, element missing qualified id error');
      end;
    end;

    Execute;

  end else
  raise exception.Create('Tween already executing error');
end;

procedure TW3Tween.Execute;
begin
  if not Active then
  begin

    FPartial := false;

    case SyncRefresh of
    true:
      begin
        // Initiate loop in 100ms
        W3_Callback( procedure ()
          begin
            W3_RequestAnimationFrame( HandleSyncUpdate );
          end, 100);
      end;
    false:
      begin
        FTimer.OnTime := HandleUpdateTimer;
        FTimer.Delay := Interval;
        FTimer.Enabled := true;
      end;
    end;

    FActive := True;
    if assigned(OnStarted) then
    OnStarted(self);

  end else
  raise exception.Create('Tween already executing error');
end;

procedure TW3Tween.Delete(const TweenIds:Array of String);
var
  Lid:  String;
  LObj: TTweenElement;
  LIndex: Integer;
begin
  if tweenIds.length>0 then
  begin
    // Only remove tweens defined in Parameter list
    for LId in Tweenids do
    begin
      LObj := ObjectOf(Lid);
      if LObj<>NIL then
      begin
        LIndex := FValues.IndexOf(LObj);
        FValues.delete(LIndex,1);
        LObj.free;
      end;
    end;
  end;
end;

Procedure TW3Tween.Resume(const index:Integer);
var
  LObj: TTweenElement;
begin
  if Active then
  begin
    if (Index>=0) and (Index<FValues.Count) then
    begin
      LObj := FValues[index];
      if LObj.State=tsPaused then
      begin
        FValues[index].StartTime := GetTimeCode;
        FValues[Index].State := tsRunning;
      end;
    end;
  end;
end;

procedure TW3Tween.Resume(const Tween:TTweenElement);
begin
  if Active then
  Begin
    if Tween<>NIL then
    Begin
      if FValues.IndexOf(Tween)>=0 then
      begin
        if Tween.state=tsPaused then
        begin
          Tween.StartTime := GetTimeCode;
          Tween.State := tsPaused;
        end;
      end;
    end;
  end;
end;

procedure TW3Tween.Resume(const Objs:Array of TTweenElement);
var
  LObj: TTweenElement;
begin
  if Active then
  begin
    if Objs.length>0 then
    Begin
      for LObj in Objs do
      begin
        if LObj<>NIl then
        Resume(LObj);
      end;
    end;
  end;
end;

procedure TW3Tween.Resume(const Ids:Array of String);
var
  LId:  String;
begin
  if Active then
  begin
    for LId in Ids do
    begin
      Resume(ObjectOf(Lid));
    end;
  end;
end;

procedure TW3Tween.Resume;
var
  LObj: TTweenElement;
begin
  if Active then
  begin
    if FValues.length>0 then
    Begin
      for LObj in FValues do
      begin
        if LObj<>NIl then
        Resume(LObj);
      end;
    end;
  end;
end;

Procedure TW3Tween.Pause(const Index:Integer);
var
  LObj: TTweenElement;
begin
  if Active then
  begin
    if (Index>=0) and (Index<FValues.Count) then
    begin
      LObj := FValues[index];
      if LObj.State=tsRunning then
      FValues[Index].State := tsPaused;
    end;
  end;
end;

procedure TW3Tween.Pause(const Tween:TTweenElement);
begin
  if Active then
  Begin
    if Tween<>NIL then
    Begin
      if FValues.IndexOf(Tween)>=0 then
      begin
        if Tween.state=tsRunning then
        Tween.State := tsPaused;
      end;
    end;
  end;
end;

procedure TW3Tween.Pause(const Objs:Array of TTweenElement);
var
  LObj: TTweenElement;
begin
  if Active then
  begin
    if Objs.length>0 then
    Begin
      for LObj in Objs do
      begin
        if LObj<>NIl then
        Pause(LObj);
      end;
    end;
  end;
end;

procedure TW3Tween.Pause(const Ids:Array of String);
var
  LId:  String;
begin
  if Active then
  begin
    for LId in Ids do
    begin
      Pause(ObjectOf(Lid));
    end;
  end;
end;

procedure TW3Tween.Pause;
var
  LObj: TTweenElement;
begin
  if Active then
  begin
    if FValues.length>0 then
    Begin
      for LObj in FValues do
      begin
        Pause(LObj);
      end;
    end;
  end;
end;

procedure TW3Tween.Cancel;
begin
  if Active then
  begin
    try
      FTimer.enabled := false;
      FTimer.OnTime := NIL;
    finally
      FActive := false;

      if assigned(OnFinished) then
      OnFinished(self);
    end;
  end;
end;

Procedure TW3Tween.Delete(index:Integer);
var
  LObj: TTweenElement;
begin
  LObj:=FValues[index];
  FValues.Delete(Index,1);
  LObj.free;
end;

procedure TW3Tween.Delete(Id:String);
begin
  Delete(FValues.indexOf(ObjectOf(Id)));
end;

function TW3Tween.Add(Id:String):TTweenElement;
begin
  Id := id.trim.lowercase;
  if id.length>0 then
  begin
    if IndexOf(Id)<0 then
    begin
      result := TTweenElement.Create;
      result.Id := Id;
      FValues.add(result);
    end else
    raise EW3Exception.CreateFmt
    ('A tween-object with id [%s] already exists in collection error',[Id]);
  end else
  raise EW3Exception.Create('Invalid tween-object id [empty] error');
end;

Function TW3Tween.Add(Id:String;const aStartValue,aDistance,aDuration:float;
          const aAnimationType:TW3TweenAnimationType;
          const aBehavior:TTweenBehavior):TTweenElement;
begin
  Id := id.trim.lowercase;
  if id.length>0 then
  begin
    if IndexOf(Id)<0 then
    begin
      result := TTweenElement.Create;
      result.StartValue := aStartValue;
      result.Distance := aDistance;
      result.Duration := aDuration;
      result.AnimationType := aAnimationtype;
      result.Behavior := aBehavior;
      result.StartTime := GetTimeCode;
      result.Id := Id;
      FValues.add(result);
    end else
    raise EW3Exception.CreateFmt
    ('A tween-object with id [%s] already exists in collection error',[Id]);
  end else
  raise EW3Exception.Create('Invalid tween-object id [empty] error');
end;

function TW3Tween.IndexOf(Id:String):Integer;
var
  x:  Integer;
begin
  result := -1;
  id := Id.trim.lowercase;
  if id.length>0 then
  begin
    for x:=0 to FValues.Count-1 do
    begin
      if Id.EqualsText(FValues[x].Id) then
      begin
        result := x;
        break;
      end;
    end;
  end;
end;

function TW3Tween.ObjectOf(const Id:String):TTweenElement;
var
  LIndex: Integer;
begin
  LIndex := IndexOf(Id);
  if LIndex>=0 then
  result := FValues[LIndex] else
  result := NIL;
end;

//############################################################################
// TTweenData
//############################################################################

function TTweenData.Expired: Boolean;
begin
  result := StartTime + Duration < GetTimeCode;
end;

Procedure TTweenData.Reset;
begin
  StartTime := 0;
  StartValue := 0;
  Distance := 0;
  Duration := 0;
  Animationtype := ttlinear;
  Behavior := tbSingle;
end;

//############################################################################
// TTweenElement
//############################################################################

Constructor TTweenElement.Create;
begin
  inherited Create;
  State := tsIdle;
end;

Procedure TTweenElement.Reset;
begin
  inherited Reset;
  State := tsIdle;
end;


//#############################################################################
//  TW3TweenEase
//#############################################################################

class function TW3TweenEase.QuartIn(t, b, c, d:float):float;
begin
  asm
    @t /= @d;
    return @c * @t * @t * @t * @t + @b;
  end;
end;

class function TW3TweenEase.QuartOut(t, b, c, d:float):float;
begin
  asm
    @t /= @d;
    @t--;
    return -@c * (@t * @t * @t * @t - 1) + @b;
  end;
end;

class function TW3TweenEase.QuartInOut(t, b, c, d:float):float;
begin
  asm
    @t /= @d/2;
    if (@t < 1) {
        return @c / 2 * @t * @t * @t * @t + @b;
    }
    @t -= 2;
    return -@c / 2 * (@t * @t * @t * @t - 2) + @b;
  end;
end;

class function TW3TweenEase.CubeInOut(t, b, c, d:float):float;
begin
  asm
    @t /= @d/2;
    if (@t < 1) {
      return @c / 2 * @t * @t * @t + @b;
    }
    @t -= 2;
    return @c/2 * (@t * @t * @t + 2) + @b;
  end;
end;

class function TW3TweenEase.CubeOut(t, b, c, d:float):float;
begin
  asm
    @t /= @d;
    @t--;
    return @c * (@t * @t * @t + 1) + @b;
  end;
end;

class function TW3TweenEase.CubeIn(t, b, c, d:float):float;
begin
  asm
    @t /= @d;
    return @c * @t * @t * @t + @b;
  end;
end;

class function TW3TweenEase.QuadInOut(t, b, c, d:float):float;
begin
  asm
    @t /= @d/2;
    if (@t < 1) {
      return @c / 2 * @t * @t * @t + @b;
    }
    @t -= 2;
    @result = @c/2*(@t * @t * @t + 2) + @b;
  end;
end;

class function TW3TweenEase.QuadOut(t, b, c, d:float):float;
begin
  asm
    @t /= @d;
    return -@c * @t * (@t - 2) + @b;
  end;
end;

class function TW3TweenEase.QuadIn(t, b, c, d:float):float;
begin
  asm
    @t /= @d;
    return @c * @t * @t + @b;
  end;
end;

class function TW3TweenEase.Linear(t,b,c,d:float):float;
begin
  asm
   @result = @c * @t / @d + @b;
  end;
end;



end.

TW3Dataset, Smart Data and what you can do

December 27, 2015 Leave a comment

In my previous post, I have requested some input from users about what they would like in the future of SMS. The question of the hour being what features or architecture would you guys like to have regarding databases and data-bound controls?

In this short post I want to write some words about what is already in Smart Mobile Studio and what you can do right now. I have also added the source code for a CSS3 powered DB grid (98% finished) that you can play with. So plenty of things in the pipeline for Smart Mobile Studio. Right, let’s get cracking and have a look at whats on the menu!

TW3Dataset

TClientDataset, not a lightweight

TClientDataset, not a lightweight

Delphi has one component which is completely and utterly unappreciated; and that is TClientDataset. This really is a fantastic piece of code (really, hear me out). First, it was written to be completely datatype agnostic and stores raw values as variants. That in itself is a monumental achievement considering that Delphi’s variants are binary compatible with Microsoft COM variants (read: sluggish, slow, and a proverbial assault on the CPU stack). Secondly, it makes use of variant-arrays to pack columns into a portable, single variant record. The author should be given a medal for not commiting suicide while creating that component. And this is just scratching the surface of TClientDataset, it really is awesome once you get the hang of it. It can act as a stand-alone, in memory only, table. It can act as a intermediate local data cache, keeping track of record changes, tagging, reverts and everything under the sun – before pushing it to a master data provider. So you can locally work with a database on the other side of the world, caching up changes and then push the whole changeset in one go.

TClientDataset is so confusing and had so much potential that Cary Jensen sat down and wrote a full 350 page book on the subject. A lot of Delphi and FreePascal developers just laugh when they hear someone mention TClientDataset, but that component is a gem if you know how to use it properly.

What about Smart?

Smart Mobile Studio ships with wrappers for WebSQL only. IndexDB will be included in the next update, but I seriously advice against using it unless you are going to package your final product with PhoneGap. Both WebSQL and IndexDB represents an onslaught of callback events, to the point of being useless in anything but JavaScript.

Only experienced and advanced Smart developers will be able to master these beasts. I have done my best to simplify their use, but I must admit I hardly ever use these myself. WebSQL is “OK” to use when you package your app with Phonegap; that takes away the 5Mb limitation and extends the procedure execution time — but other than that, all my large apps use TW3Dataset for storage.

Right. Let’s sum up what Smart Mobile Studio has to offer right now when it comes to databases and data transportation:

WebSQL

WebSQL is a small and compact SQL database. Most browsers include SQLite and simply expose that through JavaScript. In Smart Mobile Studio WebSQL is encapsulated as classes and actions in the unit SmartCL.DbSQL.pas. While WebSQL is by far the best HTML5 database, please note that it’s been marked as deprecated (meaning: it will be phased out and removed in the future). Also, it has a limitation of 5-10 megabytes per database. WebSQL is excellent if you use PhoneGap to package your final product (and you know your way around JavaScript to begin with). Phonegap removes the storage limitation and also extends the JSVM execution limitation (a procedure call must return within 2 seconds), allowing you to operate with large and complex queries.

DataSnap

Datasnap is a Delphi technology for exposing databases and RPC (remote procedure call) services to the world. Datasnap clients can call datasnap servers to obtain data, invoke methods and so on. Datasnap is an excellent way of re-using your Delphi back-end services with HTML5 or mobile applications. Smart Mobile Studio supports DataSnap out of the box, so if your company has existing datasnap databases available, your Smart applications can connect and make use of them.

It must however be mentioned that third party solutions like Mormoth offer much better performance and stability than Datasnap. Mormoth also supports Smart Mobile Studio.

Remobjects SDK

While not really a DB framework, Smart Mobile Studio can connect and invoke RO services. This opens up for some very exciting possibilities, and pushing data over an RPC framework is not hard to do. It’s also a great way to re-cycle existing RO based native services with your HTML5 or mobile applications. Smart Mobile Studio can import RO service libraries and generate Smart Pascal client interfaces for you.

TW3Dataset

This is a single table database engine written in Smart Pascal itself. It has no support for SQL or filtering (as of writing) but also no limitation on size. File storage under JSVM is however limited to the same 5-10 megabyte restriction as WebSQL, but the limitations mentioned are removed by PhoneGap. Since PhoneGap is what you want to use in getting your product onto AppStore, Google Play or Android Marketplace, the storage limitation has no real impact.

Working with TW3Dataset

Tw3Dataset was designed to be Smart Mobile Studio’s version of TClientDataset for Delphi (read: inspired but less complex) and it will no doubt grow as we establish our DB framework in 2016. You can use it to keep track of local data changes, but you have to reserve a field for that information yourself. TW3Dataset is simply a small, in memory, to the point dataset which is perfect for applications that doesnt generate huge amounts of data. It should also remain as small and compact as possible because it acts as a building-block for more complex components.

Why is this useful? Well consider this as an example: Developer Express, a great company offering a wide variety of components, sell components that mimic and implement Microsoft Outlook. You have the calendar, the day planner, the vertical scrolling menu system, the freestyle note editing; DevEx have more or less reverse engineered the visual components that make up Microsoft Outlook. The downside? Well, with such a complex, inter-connected component set, the information it generates and depends on is equally complex! So DevEx allows you to store the data directly to an existing database. They also provide a drop-in solution, in-memory tables that are created and maintained by the components for you. This is a perfect situation where a TW3Dataset would be handy to use. Rather than exposing a ton of storage events, so many that it overwhelms the developer — the components can deal with all of that and just give you a handy way of loading and saving that data.

This is the idea behind TW3Dataset. It was designed not to be complex, support SQL or any advanced features. It should be simple so people can use it to create large and complex components that can export and import data in a uniform way.

Creating a table

Before we create a table, we first have to define what the table looks like. This is done by populating the field-definitions property. TW3Dataset supports generated fields, so you can have both AutoInc and GUID fields which are generated automatically. When you have populated the field-definistions, we simply call CreateDataset() to establish the table.

var
LDataset: TW3Dataset;

LDataset.FieldDefs.Add('id',ftAutoInc);
LDataset.fieldDefs.Add('firstname',ftString);
LDataset.fieldDefs.Add('lastname',ftString);
LDataset.fieldDefs.add('text',ftString);

LDataset.CreateDataset;

Adding records

Adding records to the dataset is straight forward and more or less identical to how you would do it under Delphi. You have both append and insert operations. Let’s use Append for this example:

var
x: Integer;
LDataset: TW3Dataset;

LDataset.FieldDefs.Add('id',ftAutoInc);
LDataset.fieldDefs.Add('firstname',ftString);
LDataset.fieldDefs.Add('lastname',ftString);
LDataset.fieldDefs.add('text',ftString);
LDataset.CreateDataset;

for x:=1 to 10 do
begin
  LDataset.Append;
  LDataset.Fields.FieldByName('firstname').AsString := 'John';
  LDataset.Fields.FieldByName('firstname').AsString := 'Doe';
  LDataset.Fields.FieldByName('text').AsString:='This is #' + x.toString;
  LDataset.Post;
end;

As you can see from the code above, calling the Append() method sets the dataset in insert mode. This means it allocates the memory needed to hold the record, generates the automatic values (ID autoinc in this case) and allows access to the fields object. If you try to alter values without being in Insert or Edit mode, an exception is raised. This is standard for most languages so nothing new here. The Post() method commits the record buffer to the table, storing it in memory.

Navigating the data

Navigation is done via First, Last, Next, Back methods. You can also check for eof-of-file and beginning-of-file via traditional BOF and EOF properties. So let’s traverse the dataset we just created and dump the output to the console!

var
x: Integer;
LDataset: TW3Dataset;

LDataset.FieldDefs.Add('id',ftAutoInc);
LDataset.fieldDefs.Add('firstname',ftString);
LDataset.fieldDefs.Add('lastname',ftString);
LDataset.fieldDefs.add('text',ftString);
LDataset.CreateDataset;

for x:=1 to 10 do
begin
  LDataset.Append;
  LDataset.Fields.FieldByName('firstname').AsString := 'John';
  LDataset.Fields.FieldByName('firstname').AsString := 'Doe';
  LDataset.Fields.FieldByName('text').AsString:='This is #' + x.toString;
  LDataset.Post;
end;

LDataset.first;
while not lDataset.EOF do
begin
  var id  := LDataset.fields.fieldbyname('id').asString;
  var txt := LDataset.fields.fieldbyname('text').asString;
  writeln(id + ' ' + txt);
  LDataset.Next;
end;

And here is the output in the console:

The output is as expected in the console

The output is as expected in the console

Loading and saving

TW3Dataset allows you to save your data to a normal string or a stream. You may remember that in our last update we added TStream support as well as the possebility to allocate and work with raw memory? Well, TW3Dataset makes storage very simple. Since it exports ordinary JSON in text format, you can also use TW3Dataset as an intermediate format. It’s small enough (depending on the number of records) to be pushed to a server, and also a convenient format for retrieving X number of records from a server.

For storing datasets locally, in the browser or on your phone, just use TLocalStorage and stuff the data there. Just be aware of the limitation your browser impose on you when not running under PhoneGap (max 5-10 megabyte, the limit toggles between these depending on browser type and build number).

function    SaveToString:String;
Procedure   LoadFromString(Const aText:String);

Procedure   SaveToStream(const Stream:TStream);virtual;
Procedure   LoadFromStream(const Stream:TStream);virtual;

Grids

This has been somewhat missing in Smart Mobile Studio. Like mentioned in my previous article, we are still working on a “final” framework for databases under Smart, and a grid cannot really be created before you have some data it can bind to. But, I have actually a grid that may be of interest. It’s actually been lying around my PC since june 2015. I’m going to publish the source for this on Github later, and you can use that until we finalize the DB framework.

It makes full use of HTML5 hardware scrolling, effects and more. It’s also heavily adaptable, so you can use CSS3 animations on rows – or transform rows into something else (like clicking a row and having the row transform into an editor). It’s pretty neat! But I still need to clean it up a bit. And there is a handfull of features that must be added to the RTL before it can be used by everyone. Here is a picture of it. It doesnt capture the CSS3 animations or the animated column-dragging, but its pretty neat 🙂

A MS-Phone Skinned grid and header control

A MS-Phone Skinned grid and header control

If you want to play around with the source, here you go:

unit smartCL.dbgrid;

interface

uses 
  System.Types,
  system.types.convert,
  System.colors,
  SmartCL.system,
  SmartCL.Components,
  SmartCL.Controls.Label,
  SmartCL.Fonts,
  SmartCL.Borders,
  SmartCL.Controls.ScrollBar;

type

  TEdgeSenseControl = partial class(TW3Label);
  TGridRowContainer = partial class(TW3CustomControl);
  TCustomGrid       = partial class(TW3CustomControl);


  TEdgeRegions    = (scLeft,scTop,scRight,scBottom,scNone);
  TEdgeSenseEdges = set of TEdgeRegions;

  (*  TEdgeSenseControl:
      This control checks its own edges and changes mouse cursor
      accordingly. The edges to check is defined by the SenseEdges
      pascal SET. Use setSenseEdges() to define what edges to
      check for.

      The active edge (hovered by the mouse-pointer) is reflected
      in ActiveEdge

      To get the X/Y offset of the pointer inside an edge zone,
      call getActiveEdgeOffset()

      Edge sensebility can be disabled and enabled with
      DisableSense() and EnableSense(). *)
  TEdgeSenseControl = partial Class(TW3Label)
  const
    CNT_LEFT    = 0;
    CNT_TOP     = 1;
    CNT_RIGHT   = 2;
    CNT_BOTTOM  = 3;
    CNT_SIZE    = 10;
  private
    FEdges:   TEdgeSenseEdges;
    FRects:   Array[0..3] of TRect;
    FEdgeId:  Integer;
    FSense:   Boolean;
    procedure CheckCorners(const x,y:Integer);
    procedure UpdateCursor;
  protected
    procedure DisableSense;
    procedure EnableSense;
    function  getActiveEdge:TEdgeRegions;
    function  getActiveEdgeOffset(x,y:Integer):TPoint;
  protected
    Property  ActiveEdge:TEdgeRegions read getActiveEdge;
    Property  SenseEdges:TEdgeSenseEdges read FEdges;
    Procedure setSenseEdges(const Value:TEdgeSenseEdges);virtual;
    procedure MouseMove(shiftState:TShiftState;x,y:Integer);override;
    procedure MouseEnter(shiftState:TShiftState;x,y:Integer);override;
    procedure MouseExit(shiftState:TShiftState;x,y:Integer);override;
    procedure Resize;override;
  protected
    procedure InitializeObject;Override;
  end;

  (*  TGridHeaderColumn:
      This control inherits from TEdgeSenseControl, but adds actual
      size functionality. All column-header controls can be sized
      horizontally only.
      Where the ancestor control adds sensitivity to mouse-hovering
      over the edges, this control responds to mouse-press
      while over an edge (start size operation), and will
      adjust itself according to the user's movements *)
  TGridHeaderColumn = Class(TEdgeSenseControl)
  private
    FSizing:    Boolean;
    FMoving:    Boolean;
    FStartX:    Integer;
    FStartY:    Integer;
    FNowX:      Integer;
    FBaseWidth: Integer;
    function    getGrid:TCustomGrid;
  protected
    procedure   MouseDown(button:TMouseButton;
                shiftState:TShiftState;x,y:Integer);override;
    procedure   MouseUp(button:TMouseButton;
                shiftState:TShiftState;x,y:Integer);override;
    procedure   MouseMove(shiftState:TShiftState;x,y:Integer);override;
    procedure   InitializeObject;Override;
  public
  end;

  IGridHeader = interface
    procedure ColumnReSizeBegins(const column:TGridHeaderColumn);
    procedure ColumnReSizeEnds(const column:TGridHeaderColumn);
    procedure ColumnMoveBegins(Const column:TGridHeaderColumn);
    procedure ColumnMoveEnds(const column:TGridHeaderColumn);
    procedure ColumnSized(const column:TGridHeaderColumn);
    Procedure ColumnMoved(const Column:TGridHeaderColumn);
  end;

  TGridHeaderEvent                  = procedure (Sender:TObject;
                                      Column:TGridHeaderColumn);
  TGridHeaderColumnAddEvent         = TGridHeaderEvent;
  TGridHeaderColumnSizedEvent       = procedure (sender:TObject;
                                      Column:TGridHeaderColumn;
                                      OldSize:TPoint);
  TGridHeaderColumnMovedEvent       = TGridHeaderEvent;
  TGridHeaderColumnMoveBeginsEvent  = TGridHeaderEvent;
  TGridHeaderColumnMoveEndsEvent    = TGridHeaderEvent;
  TGridHeaderColumnSizeBeginsEvent  = TGridHeaderEvent;
  TGridHeaderColumnSizeEndsEvent    = TGridHeaderEvent;

  (*  TGridHeader:
      This is the container control for TGridHeaderColumn instances.
      It implements a simple interface for updating during a
      resize of a column.
      It also modifies TW3Component->RegisterChild to only allow
      TGridHeaderColumn instances to register. If you try to create
      other types of controls with TGridHeader as parent, it will
      result in an exception *)
  TGridHeader = Class(TW3CustomControl,IGridHeader)
  private
    FOldSize: Tpoint;
    FItems:   Array of TGridHeaderColumn;
    FOnAdded: TGridHeaderColumnAddEvent;
    FOnSized: TGridHeaderColumnSizedEvent;
    FOnMoved: TGridHeaderColumnMovedEvent;
    FOnMoveBegins:TGridHeaderColumnMoveBeginsEvent;
    FOnMoveEnds:TGridHeaderColumnMoveEndsEvent;
    FOnSizeBegins:TGridHeaderColumnSizeBeginsEvent;
    FOnSizeEnds:TGridHeaderColumnSizeEndsEvent;
  protected
    procedure ChildAdded(aChild: TW3Component);override;
    procedure ChildRemoved(aChild: TW3Component);override;
    procedure RegisterChild(aChild: TW3Component);override;
  protected
    procedure ColumnSized(const column:TGridHeaderColumn);
    procedure ColumnReSizeBegins(const column:TGridHeaderColumn);
    procedure ColumnReSizeEnds(const column:TGridHeaderColumn);
    procedure ColumnMoveBegins(Const column:TGridHeaderColumn);
    procedure ColumnMoveEnds(const column:TGridHeaderColumn);
    Procedure ColumnMoved(const Column:TGridHeaderColumn);
    procedure Resize;Override;
    Procedure StyleTagObject;override;
  public
    property  Identifier:Integer;

    Property  Columns[index:Integer]:TGridHeaderColumn
              read ( FItems[index] );
    Property  Count:Integer read ( FItems.length );
    function  Add:TGridHeaderColumn;overload;
    function  Add(Caption:String):TGridHeaderColumn;overload;
    Procedure Adjust;
    procedure Clear;

    procedure UpdateIdentifier;

    procedure FinalizeObject;Override;
  published
    Property  OnColumnAdded:TGridHeaderColumnAddEvent
              read FOnAdded write FOnAdded;
    Property  OnColumnSized:TGridHeaderColumnSizedEvent
              read FOnSized write FOnSized;
    Property  OnColumnMoved:TGridHeaderColumnMovedEvent
              read FOnMoved write FOnMoved;

    Property  OnMoveOperationBegins:TGridHeaderColumnMoveBeginsEvent
              read FOnMoveBegins write FOnMoveBegins;

    Property  OnMoveOperationEnds:TGridHeaderColumnMoveEndsEvent
              read FOnMoveEnds write FOnMoveEnds;

    Property  OnSizeOperationBegins:TGridHeaderColumnSizeBeginsEvent
              read FOnSizeBegins write FOnSizeBegins;

    Property  OnSizeOperationEnds:TGridHeaderColumnSizeEndsEvent
              read FOnSizeEnds write FOnSizeEnds;
  end;

  TGridVerticalScrollbar = Class(TW3VerticalScrollbar)
  end;

  TGridDataItem   = class(TW3CustomControl)
  public
    Property  ColumnItem:TGridHeaderColumn;
  end;

  (* This class represents a single column in a row.
     It is created as a child of TW3GridDataRow. *)
  TGridDataColumn = Class(TGridDataItem)
  protected
    procedure   InitializeObject;Override;
  end;

  (* This class represents the left-most edit cursor *)
  TGridEditorColumn = Class(TGridDataItem)
  end;

  IGridDataRow = Interface
    Procedure   Populate;
  end;

  TW3GridDataRow  = Class(TGridDataItem,IGridDataRow)
  private
    FSelected:  Boolean;
    FEditCol:   TGridEditorColumn;
    Procedure   Populate;
  protected
    function    getGrid:TCustomGrid;
    procedure   setSelected(Const Value:Boolean);
    procedure   Resize;Override;
    procedure   InitializeObject;Override;
    procedure   FinalizeObject;Override;
  protected
    procedure   MouseDown(button:TMouseButton;
                shiftState:TShiftState;x,y:Integer);override;
  public
    Property  Parent:TGridRowContainer read (TGridRowContainer(inherited Parent));

    Property    Index:Integer;
    Property    Identifier:Integer;
    Property    Column[index:Integer]:TGridDataColumn
                read ( TGridDataColumn(getChildObject(index)) );
    Property    Count:Integer read ( getChildCount );
    Property    Selected:Boolean read FSelected write setSelected;

    procedure   UpdateIdentifier;
    procedure   Update;

  end;

  TGridRowInfo = Record
    DOM:    TW3GridDataRow;
  end;

  (*  TGridRowContainer
      =================
      This is a container control which is created as a direct child
      element on the grid. All rows are created inside this container, and
      when scrolling occurs - it's actually this container which we target
      for scrolling *)
  TGridRowContainer = Class(TW3CustomControl)
  public
    Property  Parent:TCustomGrid read (TCustomGrid(inherited Parent));

    Property  Count:Integer read ( inherited getChildCount );
    Property  Rows[index:Integer]:TW3GridDataRow read
              ( TW3GridDataRow( inherited getChildObject(index) ) );default;
  end;

  TGridOptions = class(TW3OwnedObject)
  private
    FScrollDelay: Integer;
  protected
    procedure setScrollDelay(Value:Integer);virtual;
  public
    Property  Owner:TCustomGrid read (TCustomGrid(inherited Owner));
    Property  AllowSelect:Boolean;
    property  AllowColSize:Boolean;
    Property  AllowColMove:Boolean;
    Property  RowSelect:Boolean;
    Property  ShowEditCol:Boolean;
    Property  ScrollDelay:Integer read FScrollDelay write setScrollDelay;
  end;

  IGridStyler = Interface
    procedure StyleRow(Const Index:Integer;
              Const Row:TW3GridDataRow);
    procedure StyleColumn(const Index:Integer;
              const Col:TGridDataColumn);
    procedure StyleClientArea(const control:TGridRowContainer);
    procedure StyleNonClientArea(const control:TCustomGrid);
    Procedure RowSelected(const Index:Integer;
              const Row:TW3GridDataRow);
    procedure RowUnselected(const Row:TW3GridDataRow);
    procedure ColumnSelected(const index:Integer;
              const Column:TGridDataColumn);
    procedure ColumnUnSelected(Column:TGridDataColumn);
  end;

  TGridStyler = Class(TObject,IGridStyler)
  protected
    procedure StyleRow(Const Index:Integer;
              Const Row:TW3GridDataRow);virtual;

    procedure StyleColumn(const Index:Integer;
              const Col:TGridDataColumn);virtual;

    procedure StyleClientArea(const control:TGridRowContainer);virtual;

    procedure StyleNonClientArea(const control:TCustomGrid);virtual;

    Procedure RowSelected(const Index:Integer;
              const Row:TW3GridDataRow);virtual;

    procedure RowUnselected(const Row:TW3GridDataRow);virtual;

    procedure ColumnSelected(const index:Integer;
              const Column:TGridDataColumn);virtual;

    procedure ColumnUnSelected(Column:TGridDataColumn);virtual;
  end;

  TGridTools = Class(TW3CustomControl)
  end;

  TCustomGrid = Class(TW3CustomControl)
  const
    CNT_CREATE_DELAY  = 100;
  private
    FRowInfo: Array of TGridRowInfo;  // LUT for items
    FExInfo:  Array of Integer;       // LUT for items with custom height
    FStack:   Array of Integer;
    FHeader:  TGridHeader;
    FRowSize: Integer = 24;
    FContainer: TGridRowContainer;
    FTools:   TGridTools;
    FVScroll: TGridVerticalScrollbar;
    FOptions: TGridOptions;
    procedure HandleScroll(Sender:TObject);
    procedure ProcessStack;
  protected

    procedure HandleHeaderColumnMoved
              (Sender:TObject;Column:TGridHeaderColumn);virtual;
    procedure HandleHeaderColumnSized(Sender:TObject;Column:TGridHeaderColumn;
              OldSize:TPoint);virtual;
  protected
    function  getPageSize:Integer;
    procedure setGeneralRowHeight(const Value:Integer);virtual;
    function  getContentHeight:Integer;
    function  getTopItemIndex:Integer;
    function  getOffsetForItem(Const RowIndex:Integer):Integer;

    Procedure Render;
  protected
    procedure Resize;override;
    procedure InitializeObject;override;
    procedure FinalizeObject;Override;
  public
    Property  Options:TGridOptions read FOptions;
    Property  RowHeight:Integer read FRowSize write setGeneralRowHeight;
    Property  Header:TGridHeader read FHeader;

    procedure ShowTools;

    function  IndexOfRow(Const Row:TW3GridDataRow):Integer;

    function  Add(const Index:Integer):TW3GridDataRow;
    procedure Allocate(Rows:Integer);
    procedure Clear;
  end;


implementation

uses  system.memory,
      system.streams,
      system.dateutils,
      system.types.convert;

//############################################################################
// TGridStyler
//############################################################################


procedure TGridStyler.StyleRow(Const Index:Integer;
          Const Row:TW3GridDataRow);
begin
  case index mod 2 of
  0:  Row.background.fromColor(clRed);
  1:  row.background.fromColor(clWhite);
  end;
end;

procedure TGridStyler.StyleColumn(const Index:Integer;
          const Col:TGridDataColumn);
begin
end;

procedure TGridStyler.StyleClientArea(const control:TGridRowContainer);
begin
end;

procedure TGridStyler.StyleNonClientArea(const control:TCustomGrid);
begin
end;

Procedure TGridStyler.RowSelected(const Index:Integer;
          const Row:TW3GridDataRow);
begin
end;

procedure TGridStyler.RowUnselected(const Row:TW3GridDataRow);
begin
end;

procedure TGridStyler.ColumnSelected(const index:Integer;
          const Column:TGridDataColumn);
begin
end;

procedure TGridStyler.ColumnUnSelected(Column:TGridDataColumn);
begin
end;
//############################################################################
// TW3GridDataRow
//############################################################################

procedure TGridDataColumn.InitializeObject;
begin
  inherited;
  self.Font.Size:=12;
  self.Font.Color:=clWhite;
end;

//############################################################################
// TW3GridDataRow
//############################################################################

procedure TW3GridDataRow.InitializeObject;
begin
  inherited;
  (Handle)['onmousedown'] := @CBMouseDown;
end;

procedure TW3GridDataRow.FinalizeObject;
begin
  if FEditCol<>NIL then
  FEditCol.free;
  inherited;
end;

procedure TW3GridDataRow.MouseDown(button:TMouseButton;
          shiftState:TShiftState;x,y:Integer);
begin
  inherited MouseDown(Button,ShiftState,x,y);

  if  assigned(parent)
  and assigned(parent.parent) then
  begin

    parent.parent.showTools;

  end;
  Selected:=not Selected;

end;

procedure TW3GridDataRow.Update;
begin
  Beginupdate;
  AddToComponentState([csSized]);
  EndUpdate;
end;

procedure TW3GridDataRow.UpdateIdentifier;
var
  x:  Integer;
begin
  Identifier:=0;
  for x:=0 to self.Count-1 do
  Identifier:=Identifier + ((Column[x].Width + Column[x].left)  shl x);
end;

function TW3GridDataRow.getGrid:TCustomGrid;
begin
  result:=NIL;
  if  (parent<>NIL)
  and (parent.parent<>NIL) then
  result:=TCustomGrid(parent.parent);
end;

procedure TW3GridDataRow.Resize;
var
  x:  integer;
begin
  inherited;
  if FEditCol<>NIL then
  Begin
    FEditCol.setBounds(0,0,Height,Height);
  end;

  for x:=0 to Count-1 do
  begin
    var mItem:=self.Column[x];
    if mItem.ColumnItem<>NIL then
    begin
      (* mItem.fxScaleTo
        (
        mItem.ColumnItem.Left,
        0,
        mItem.ColumnItem.Width,
        clientHeight,
        0.2
        ); *)

      mItem.SetBounds
        (
          mItem.ColumnItem.Left,
          0,
          mItem.ColumnItem.Width,
          clientHeight
        );
    end;
  end;
end;

Procedure TW3GridDataRow.Populate;
var
  x:  Integer;

  function  RandomColor:TColor;
  begin
    result:=RGBToColor
      (
        round( Random * 255 ),
        round( random * 255 ),
        round( random * 255 )
      );
  end;

begin
  if Parent<>NIL then
  begin
    var mGrid:=self.getGrid;
    if mGrid<>NIL then
    begin
      (* Create edit-cursor column? *)
      if mGrid.Options<>NIL then
      Begin
        if mGrid.Options.ShowEditCol then
        FEditCol:=TGridEditorColumn.Create(self);
      end;

      var rowColor := clNone;
      if index>=0 then
      begin
        if Index mod 2=1 then
        StyleClass:='RowOdd' else
        StyleClass:='RowEven';

        if index mod 2=1 then
        RowColor:=RGBToColor(33,33,33) else
        rowcolor:=RGBToColor(0,0,0);
      end;

      (* Now create columns based on header *)
      for x:=0 to mgrid.header.count-1 do
      Begin
        var mItem:=TGridDataColumn.Create(self);
        mItem.ColumnItem:=mGrid.Header.Columns[x];
        if x in [0,2] then
        mItem.Background.fromColor(RGBToColor(55,55,55));
      end;

      //handle.style['background-color']:=ColorToWebStr(rowColor);
      background.FromColor(rowColor);

      (* Update identifier, used by the grid to know
         if a row needs a resize or have a different layout
         from the main-form *)
      UpdateIdentifier;
    end;
  end;
end;

procedure TW3GridDataRow.setSelected(Const Value:Boolean);
begin
  if value<>FSelected then
  begin
    FSelected:=Value;
    case Value of
    True:   CSSClasses.Add("RowSelected");
    false:  CSSClasses.RemoveByName("RowSelected");
    end;
  end;
end;


//############################################################################
// TGridOptions
//############################################################################

procedure TGridOptions.setScrollDelay(Value:Integer);
begin
  value:=TInteger.EnsureRange(Value,0,100);
  if value<>FScrollDelay then
  begin
    FScrollDelay:=Value;
  end;
end;

//############################################################################
// TCustomGrid
//############################################################################

procedure TCustomGrid.InitializeObject;
begin
  inherited;

  FOptions:=TGridOptions.Create(self);

  FHeader:=TGridHeader.Create(self);

  FVScroll:=TGridVerticalScrollbar.Create(self);
  FVScroll.OnChanged:=HandleScroll;
  FVScroll.Enabled:=False;
  FVScroll.width:=24;
  FVScroll.Background.FromColor(clWhite);

  FContainer:=TGridRowContainer.Create(self);
  FContainer.Background.FromColor(clGreen);

  FHeader.OnColumnMoved:=HandleHeaderColumnMoved;
  FHeader.OnColumnSized:=HandleHeaderColumnSized;
end;

procedure TCustomGrid.FinalizeObject;
begin
  FContainer.free;
  FVScroll.free;
  FHeader.clear;
  FHeader.free;
  FOptions.free;
  inherited;
end;

procedure TCustomGrid.HandleHeaderColumnSized
              (Sender:TObject;Column:TGridHeaderColumn;OldSize:TPoint);
begin
  Header.UpdateIdentifier;
  Render;
end;

procedure TCustomGrid.HandleHeaderColumnMoved
          (Sender:TObject;Column:TGridHeaderColumn);
var
  x:  Integer;
begin
  var mIndex:=getTopItemIndex;
  var mPage:=self.getPageSize;

  Header.UpdateIdentifier;
  //Render;

  for x:=0 to mPage-1 do
  begin
    var mRow:=FRowInfo[mIndex + x].DOM;
    if (mRow<>NIL) then
    begin
      mRow.BeginUpdate;
      mRow.addToComponentState([csSized]);
      mRow.EndUpdate;
    end;
  end;
end;

procedure TCustomGrid.setGeneralRowHeight(const Value:Integer);
begin
  FRowSize:=TInteger.EnsureRange(Value,12,128);
end;

procedure TCustomGrid.Clear;
var
  x:  Integer;
begin
  try
    for x:=0 to self.FRowInfo.length-1 do
    begin
      if FRowInfo[x].DOM<>NIL then
      FRowInfo[x].DOM.free;
    end;
  finally
    FRowInfo.Clear;
  end;
end;

function TCustomGrid.getTopItemIndex:Integer;
begin
  result:=FVScroll.Position;
end;

function TCustomGrid.getOffsetForItem(Const RowIndex:Integer):Integer;
begin
  result:=RowIndex * FRowSize;
end;

function TCustomGrid.getPageSize:Integer;
Begin
  result:=FContainer.ClientHeight div FRowSize;
end;

function TCustomGrid.getContentHeight:Integer;
var
  x:  Integer;
begin
  result:=(FRowInfo.Length-FExInfo.length) * FRowSize;
  for x:=0 to FExInfo.length-1 do
  inc(result,FRowInfo[x].DOM.height);
end;

procedure TCustomGrid.ProcessStack;
var
  mItem:  TW3GridDataRow;
  mIndex: Integer;
Begin
  if FStack.Count>0 then
  begin
    (* We leave the number on the stack, that way we dont end
       up creating duplicates, which would drain memory line insane *)
    mIndex:=FStack.Peek;
    try
      if FRowInfo[mIndex].DOM=NIL then
      Begin
        mItem:=Add(mIndex);
        FRowInfo[mIndex].DOM:=mItem;

        mItem.Column[0].InnerHTML:=IntToStr(mIndex);
        mItem.SetBounds
        (
          0,
          getOffsetForItem(mIndex),
          clientwidth - self.FVScroll.width,
          FRowSize
        );
      end;
    finally
      FStack.Pop;
    end;

    if FStack.count>0 then
    ProcessStack;
  end;
end;

Procedure TCustomGrid.Render;
var
  x:  Integer;
  mTopIndex:  Integer;
  mPageItems: Integer;
  mObj:TW3GridDataRow;
begin

  mTopIndex:=getTopItemIndex;
  mPageItems:=FContainer.Height div FRowSize;

  for x:=0 to mPageItems-1 do
  begin
    var mIndex:= x + mTopIndex;

    (* Get allocated row object *)
    mObj:=FRowInfo[mIndex].DOM;

    (* No allocated row? Push to stack, late create *)
    if mObj=NIL then
    begin
      if FStack.IndexOf(mIndex)=-1 then
      FStack.Push(mIndex);
    end else
    begin
      (* Is the row identifier different from the header layout? *)
      if mObj.Identifier<>FHeader.Identifier then
      begin
        mObj.UpdateIdentifier;
        mObj.update;
      end;

    end;
  end;

  if FStack.Length>0 then
  ProcessStack;
end;

procedure TCustomGrid.Allocate(Rows:Integer);
begin
  if FRowInfo.length>0 then
  Clear;

  if Rows>0 then
  begin
    FRowInfo.Clear;
    FRowInfo.SetLength(Rows);
    writeln("Allocating " + rows.tostring + " rows");

    FVScroll.Enabled:=true;
    FVScroll.Total:=Rows;
    FVScroll.Position:=0;
    FVScroll.PageSize:=getPageSize;
    FVScroll.Visible:=true;

    w3_requestAnimationFrame(Render);
  end;
end;

function TCustomGrid.IndexOfRow(Const Row:TW3GridDataRow):Integer;
var
  x:  Integer;
begin
  result:=-1;
  for x:=0 to FRowInfo.Length do
  begin
    if FRowInfo[x].DOM = Row then
    Begin
      result:=x;
      break;
    end;
  end;
end;

procedure TCustomGrid.ShowTools;
var
  mHeight:  integer;
  mTargetPos: integer;
  mTestPos:   Integer;
begin
  if not (csDestroying in ComponentState) then
  begin
    if FRowInfo.Length>0 then
    begin
      mTargetPos:=(ClientHeight - FHeader.Height) div 2;
      mTestPos  :=FHeader.top + FHeader.height + 10;

      mHeight:=(ClientHeight-FHeader.height) div 2;

      if FContainer.top>=mTestPos then
      FContainer.fxScaleTo(0,FHeader.height,FContainer.Width,clientHeight-Fheader.Height,0.3,
        procedure ()
        begin
          FVScroll.PageSize:=getPageSize;
        end)
      else
      FContainer.fxScaleTo(0,mHeight,FContainer.width,FContainer.height ,0.4,
        procedure ()
        begin
          FContainer.height := (ClientHeight -mHeight) - 20;
          FVScroll.PageSize:=getPageSize;
        end);
    end;
  end;
end;


function TCustomGrid.Add(const Index:Integer):TW3GridDataRow;
Begin
  result:=TW3GridDataRow.Create(FContainer);
  result.setBounds(
    0,
    Header.BoundsRect.bottom + getContentHeight,
    FContainer.clientwidth,
    FRowSize);

    result.Index:=Index;

  result.BeginUpdate;
  try
    (result as IGridDataRow).populate;
  except
    on e: exception do
    begin
      result.free;
      result:=NIL;
      exit;
    end;
  end;
  result.AddToComponentState([csSized]);
  result.EndUpdate;
end;

procedure TCustomGrid.HandleScroll(Sender:TObject);
begin
  if (csReady in ComponentState) then
  Begin
    FContainer.ScrollInfo.ScrollTo(0,FVScroll.Position * FRowSize);
    if FOptions.ScrollDelay=0 then
    Render else
    w3_setTimeout(Render,FOptions.ScrollDelay);
  end;
end;

procedure TCustomGrid.Resize;
var
  mScaledHeight:  Integer;
begin
  inherited;
  If Handle.Valid
  and handle.ready then
  begin
    FHeader.SetBounds(0,0,clientwidth,32);

    FVScroll.setBounds(
      clientwidth-FVScroll.width,
      FHeader.height,
      FVScroll.width,
      clientheight - Fheader.height);
      //mScaledHeight:=mScaledHeight * FRowSize;
    //end else

    mScaledHeight:=clientHeight - (FHeader.Height);

    FContainer.setBounds(0,FHeader.height,
      clientWidth-FVScroll.width,
      mScaledHeight);
  end;
end;

//############################################################################
// TGridHeader
//############################################################################

procedure TGridHeader.FinalizeObject;
begin
  inherited;
end;

Procedure TGridHeader.styleTagObject;
Begin
  inherited;
end;

procedure TGridHeader.Resize;
var
  wd: Integer;
  x:  Integer;
  dx: Integer;
  cnt:  Integer;
begin
  cnt:=Count;
  if cnt>0 then
  begin
    if handle.valid
    and handle.ready then
    begin
      dx:=0;
      wd:=clientWidth div cnt;
      for x:=0 to cnt-1 do
      begin
        wd:=Columns[x].width;
        Columns[x].setBounds(dx,0,wd,clientHeight);
        inc(dx,wd);
      end;
    end;
  end;
end;

procedure TGridHeader.RegisterChild(aChild: TW3Component);
begin
  if (aChild<>NIL) then
  begin
    if (aChild is TGridHeaderColumn) then
    inherited RegisterChild(aChild) else
    Raise Exception.Create
    ('Only column controls can be added to a grid header');
  end;
end;

procedure TGridHeader.ChildAdded(aChild: TW3Component);
begin
  inherited ChildAdded(aChild);
  FItems.Add(TGridHeaderColumn(aChild));
  if not (csDestroying in ComponentState)
  and not (csLoading in ComponentState) then
  Resize;

end;

procedure TGridHeader.ChildRemoved(aChild: TW3Component);
var
  mIndex: Integer;
begin
  inherited ChildRemoved(aChild);
  mIndex:=FItems.IndexOf(TGridHeaderColumn(aChild));
  if mIndex>=0 then
  FItems.delete(mIndex,1);
  if not (csDestroying in ComponentState)
  and not (csLoading in ComponentState) then
  Resize;
end;

procedure TGridHeader.ColumnReSizeBegins(const column:TGridHeaderColumn);
begin
  FOldSize:=TPoint.Create(Column.Width,Column.Height);
  if assigned(FOnSizeBegins) then
  FOnSizeBegins(self,Column);
end;

procedure TGridHeader.ColumnReSizeEnds(const column:TGridHeaderColumn);
begin
  if assigned(FOnSized) then
  FOnSized(self,Column,
  FOldSize);
  if assigned(FOnSizeEnds) then
  FOnSizeEnds(self,Column);
end;

procedure TGridHeader.ColumnMoveBegins(Const column:TGridHeaderColumn);
begin
  if assigned(FOnMoveBegins) then
  FOnMoveBegins(self,Column);
end;

procedure TGridHeader.ColumnMoveEnds(const column:TGridHeaderColumn);
begin
  if assigned(FOnMoveEnds) then
  FOnMoveEnds(self,Column);
end;

Procedure TGridHeader.ColumnMoved(const Column:TGridHeaderColumn);

  function GetChildrenSortedByXPos: Array of TGridHeaderColumn;
  var
    mCount: Integer;
    x:  Integer;
    mAltered: Boolean;
    mObj: TGridHeaderColumn;
    mLast:  TGridHeaderColumn;
    mCurrent: TGridHeaderColumn;
  begin
    mCount := GetChildCount;

    if mCount>0 then
    begin
      (* populate list *)
      for x := 0 to mCount - 1 do
      begin
        mObj := Columns[x];
        if (mObj is TGridHeaderColumn) then
        Result.add(mObj);
      end;

      (* sort by X-pos *)
      if Result.Count>1 then
      begin
        repeat
          mAltered := False;
          for x := 1 to mCount - 1 do
          begin
            mLast := TGridHeaderColumn(Result[x - 1]);
            mCurrent := TGridHeaderColumn(Result[x]);
            if
              mCurrent.left + (mCurrent.width div 2)
              <
              mLast.left + (mLast.width div 2) then
            begin
              Result.Swap(x - 1,x);
              mAltered := True;
            end;
          end;
        until mAltered=False;
      end;
    end;
  end;


begin
  var mItems := GetChildrenSortedByXPos;

  var x:=0;
  var dx:=0;
  var mWaits:=mItems.count;

  for x:=0 to mItems.count-1 do
  begin
    mItems[x].fxMoveTo(dx,mItems[x].Top,0.3, procedure ()
      begin
        dec(mWaits);
        if mWaits=0 then
        begin
          if assigned(FOnMoved) then
          FOnMoved(self,Column);
        end;
      end);
    inc(dx,mItems[x].width);
  end;
  FItems:=mItems;
end;

procedure TGridHeader.ColumnSized(const column:TGridHeaderColumn);
begin
  w3_requestAnimationFrame(Resize);
end;

Procedure TGridHeader.Adjust;
begin
  Resize;
end;

procedure TGridHeader.Clear;

  procedure doClear;
  var
    x:  Integer;
    mItem:  TGridHeaderColumn;
  begin
    for x:=0 to getChildCount-1 do
    Begin
      if (getChildObject(x) is TGridHeaderColumn) then
      Begin
        mItem:=TGridHeaderColumn(getChildObject(x));
        mItem.free;
      end;
    end;
  end;

begin
  if not (csDestroying in ComponentState) then
  begin
    BeginUpdate;
    try
      doClear;
    finally
      addToComponentState([csSized]);
      endUpdate;
      UpdateIdentifier;
    end;
  end else
  doClear;
end;

procedure TGridHeader.UpdateIdentifier;
var
  x:  Integer;
begin
  (* Calculate new identifier *)
  Identifier:=0;
  for x:=0 to FItems.Length-1 do
  Identifier:=Identifier + ((Columns[x].Width + Columns[x].left)  shl x);
end;

function TGridHeader.Add:TGridHeaderColumn;
begin
  beginupdate;
  result:=TGridHeaderColumn.Create(self);
  result.width:=100;
  result.height:=clientHeight;
  addToComponentState([csSized]);
  endupdate;
  UpdateIdentifier;
  w3_setTimeOut( procedure ()
    begin
      if assigned(FOnAdded) then
      FOnAdded(self,result);
    end,
    10);
end;

function TGridHeader.Add(Caption:String):TGridHeaderColumn;
begin
  beginupdate;
  result:=TGridHeaderColumn.Create(self);
  result.width:=100;
  result.height:=clientHeight;
  result.caption:=Caption;
  addToComponentState([csSized]);
  endupdate;
  UpdateIdentifier;

  w3_setTimeOut( procedure ()
    begin
      if assigned(FOnAdded) then
      FOnAdded(self,result);
    end,
    10);
end;

//############################################################################
// TGridHeaderColumn
//############################################################################

procedure TGridHeaderColumn.InitializeObject;
begin
  inherited;
  (* Manually initialize event handlers *)
  (Handle)['onmousedown'] := @CBMouseDown;
  (Handle)['onmouseup'] := @CBMouseUp;

  setSenseEdges([scRight]);
  Caption:='Column';

  font.Name:="verdana";
  font.size:=12;
  font.color:=clWhite;

end;

procedure TGridHeaderColumn.MouseDown(button:TMouseButton;
          shiftState:TShiftState;x,y:Integer);
begin
  inherited MouseDown(button,shiftstate,x,y);
  if (ActiveEdge in [scLeft,scRight]) then
  begin
    DisableSense;
    setCapture;
    FSizing:=True;
    FStartX:=x;
    FBaseWidth:=Width;
    (Parent as IGridHeader).ColumnReSizeBegins(self);
  end else
  Begin
    if clientRect.ContainsPos(x,y) then
    Begin
      FMoving:=True;
      DisableSense;
      setCapture;
      FStartX:=x;
      FStartY:=y;
      w3_setStyle(Handle,'cursor','move');
    (Parent as IGridHeader).ColumnMoveBegins(self);
    end;
  end;
end;

procedure TGridHeaderColumn.MouseMove(shiftState:TShiftState;x,y:Integer);
var
  dx: Integer;
begin
  inherited MouseMove(ShiftState,x,y);
  if FSizing then
  begin
    FNowX:=x;
    dx:=FNowX - FStartX ;

    SetWidth(FBaseWidth + dx);
    (TGridHeader(parent) as IGridHeader).ColumnSized(self);
  end;

  if FMoving then
  begin
    left:=self.ClientToScreen( TPoint.Create(x - FStartX,top) ).x;
  end;
end;

function TGridHeaderColumn.getGrid:TCustomGrid;
begin
  result:=NIL;
  if Parent<>NIL then
  Begin
    if Parent.parent<>NIL then
    result:=TCustomGrid(parent.Parent);
  end;
end;

procedure TGridHeaderColumn.MouseUp(button:TMouseButton;
          shiftState:TShiftState;x,y:Integer);
begin
  inherited MouseUp(button,shiftstate,x,y);
  if FSizing then
  Begin
    FSizing:=False;
    ReleaseCapture;
    EnableSense;
    (Parent as IGridHeader).ColumnSized(self);
    (Parent as IGridHeader).ColumnReSizeEnds(self);
  end;

  if FMoving then
  begin
    FMoving:=false;
    ReleaseCapture;
    EnableSense;
    w3_setStyle(Handle,'cursor','default');
    (Parent as IGridHeader).ColumnMoved(self);
  end;
end;


//############################################################################
// TEdgeSenseControl
//############################################################################

Procedure TEdgeSenseControl.initializeObject;
Begin
  inherited;
  FEdges:=[scLeft,scTop,scRight,scBottom];

  (* Manually hook up events *)
  (Handle)['onmousemove'] := @CBMouseMove;
  (Handle)['onmouseover'] := @CBMouseEnter;
  (Handle)['onmouseout'] := @CBMouseExit;

  FSense:=True;

  AlignText:=TTextAlign.taCenter;
end;

Procedure TEdgeSenseControl.setSenseEdges(const Value:TEdgeSenseEdges);
begin
  if (scLeft in Value) then Include(FEdges,scLeft) else Exclude(FEdges,scLeft);
  if (scTop in Value) then include(FEdges,scTop) else Exclude(FEdges,scTop);
  if (scRight in Value) then Include(FEdges,scRight) else
  Exclude(FEdges,scRight);
  if (scBottom in Value) then include(FEdges,scBottom) else
  Exclude(FEdges,scBottom);
end;

procedure TEdgeSenseControl.Resize;
begin
  inherited;
  FRects[0]:=TRect.CreateSized(0,CNT_SIZE,CNT_SIZE,ClientHeight-CNT_SIZE);
  FRects[1]:=TRect.CreateSized(0,0,ClientWidth,CNT_SIZE);
  FRects[2]:=TRect.CreateSized(ClientWidth-CNT_SIZE,CNT_SIZE,Clientwidth,ClientHeight-CNT_SIZE);
  FRects[3]:=TRect.CreateSized(0,ClientHeight-CNT_SIZE,ClientWidth,CNT_SIZE);
end;

procedure TEdgeSenseControl.CheckCorners(const x,y:Integer);
var
  mItem:TRect;
  mIndex: Integer;
begin
  FEdgeId:=-1;
  for mItem in FRects do
  begin
    if mItem.ContainsPos(x,y) then
    begin
      FEdgeId:=mIndex;
      break;
    end;
    inc(mIndex);
  end;
end;

function TEdgeSenseControl.getActiveEdgeOffset(x,y:Integer):TPoint;
begin
  if FEdgeId>=0 then
  result:=TPoint.Create
    ( x - FRects[FEdgeId].Left,
      y - FRects[FEdgeId].Top
    );
end;

function TEdgeSenseControl.getActiveEdge:TEdgeRegions;
begin
  if (FEdgeId>=0) and (FEdgeId<=3) then
  result:=TEdgeRegions(FEdgeid) else
  result:=scNone;
end;

procedure TEdgeSenseControl.UpdateCursor;
const
  CN_LEFT   = 0;
  CN_TOP    = 1;
  CN_RIGHT  = 2;
  CN_BOTTOM = 3;
var
  mCursor:  string;

  procedure doDefault;
  begin
    if mCursor<>'default' then
    w3_setStyle(Handle,'cursor','default');
  end;

begin
  mCursor:=w3_getStyleAsStr(Handle,'cursor').LowerCase;

  case FEdgeId of
    CN_LEFT:
      Begin
        if (scLeft in FEdges) then
        begin
          if mCursor<>'col-resize' then
          w3_setStyle(Handle,'cursor','col-resize');
        end else
        doDefault;
      end;
    CN_RIGHT:
      Begin
        if (scRight in FEdges) then
        begin
          if mCursor<>'col-resize' then
          w3_setStyle(Handle,'cursor','col-resize');
        end else
        doDefault;
      end;
    CN_TOP:
      begin
        if (scTop in FEdges) then
        begin
          if mCursor<>'row-resize' then
          w3_setStyle(Handle,'cursor','row-resize');
        end else
        doDefault;
      end;
    CN_BOTTOM:
      begin
        if (scBottom in FEdges) then
        begin
          if mCursor<>'row-resize' then
          w3_setStyle(Handle,'cursor','row-resize');
        end else
        doDefault;
      end;
    else
    doDefault;
  end;
end;

procedure TEdgeSenseControl.DisableSense;
begin
  FSense:=False;
end;

procedure TEdgeSenseControl.EnableSense;
begin
  FSense:=True;
end;

procedure TEdgeSenseControl.MouseMove(shiftState:TShiftState;x,y:Integer);
begin
  if FSense then
  begin
    CheckCorners(x,y);
    UpdateCursor;
  end;
  inherited MouseMove(ShiftState,x,y);
end;

procedure TEdgeSenseControl.MouseEnter(shiftState:TShiftState;x,y:Integer);
begin
  if FSense then
  begin
    CheckCorners(x,y);
    UpdateCursor;
  end;
  inherited MouseEnter(ShiftState,x,y);
end;

procedure TEdgeSenseControl.MouseExit(shiftState:TShiftState;x,y:Integer);
begin
  if FSense then
  Begin
    CheckCorners(x,y);
    UpdateCursor;
  end;
  inherited MouseExit(ShiftState,x,y);
end;

end.

Notes on the grid

The reason the grid is able to deal with hundreds of rows quickly, is because i use a stack technique. A row is only allocated once it steps into view, or is assigned data.

You are more than welcome to play around with it. Just keep in mind that its experimental at this stage, and the final version will no doubt be more polished.

Well, hope you got inspired! Enjoy, and merry xmas!

Smart platform services

November 4, 2015 2 comments

Something cool is taking place in the Smart Mobile Studio RTL, namely the implementation of platform services.

What is a platform service you say? Put simply a platform service is an API that the RTL exposes for you while abstracting you from the low-level implementation. Where the existing browser-driver system is platform spesific, dealing with how the browser exposes it’s features (which is different from browser to browser), platform services are feature spesific: meaning that they present a set of features un-bound by the environment.

This means that even though browsers implement things differently (even with different names, attributes and method calls), you dont have to worry about it.

Platform services are available (read: resolved) at runtime rather than design-time. So you dont have to pick out the services from a list prior to compilation, instead the RTL will create the correct object factory – which in turn exposes the correct service-item for the browser you use. The number of services available depends on what you have used, which the compiler takes care of.

What features?

Services deal with features that the browsers may or may-not expose, hence the service architecture for querying what you can and cannot do. For instance, access to the camera or image-picker is a typical service API, because it greatly depends on the mobile-device, what JS API’s are available (phonegap or native access) – and last but not least: user interaction.

Other types of services are more low-level in nature: websocket support, rest-client support, JSONP support, data encoding mechanisms, encryption support, access to gps tracking, camera access, disk access, storage functionality, database access.

if PlatformServices.GetCameraAPI(IAccess) then
begin
  if IAccess.ShowCamera(mediastream, [ocTakePhoto,ocPhotoPicker]) then
  begin
    //mediastream now contains picture or movie frames
  end else
  writeln('debug: camera was canceled');
end else
raise exception.create('Camera not supported error');

Now in order for us to shield you from the gory details we have started to implement the most common API’s as run-time services. This means easy to use, easier for us to maintain and setting a standard for future expansion of the Smart Mobile Studio run-time library.

Working with platform services

Coding based on platform services is all about abstraction. You simply ask the system to provide you with access to a service, and if the system supports that service (or allows it) you are given an access interface. The abstraction layer and working with interfaces like this is very effective. You can of course create class instances ad-hoc if you like, but that kinda defeats the portability of your code and binds you to that spesific class (which may be webkit spesific, firefox spesific or something else). The goal of platform services is to make sure your code runs and behaves identical on all supported platforms.

Websocket example

Here is an example of how to access the websocket platform service, using the access interface to create a websocket element, initialize it and then connect to a server:

procedure TForm1.btnConnectClick(Sender: TObject);
var
  IAccess:  IW3WebSocketService;
  ISocket:  IWebSocket;
begin
  if PlatformServices.GetWebSocketAPI(IAccess) then
  begin
    if IAccess.CreateSocket(ISocket) then
    Begin

      //Attach connected event handler
      ISocket.SetOnOpenEvent( procedure (Access:IWebSocket)
        begin
          Writeln('Websocket is now open!');
        end);

      // Attach disconnect event handler
      ISocket.SetOnClosedEvent( procedure (Access:IWebSocket)
        begin
          Writeln('Websocket is now closed');
        end);

      // Attach Message handler
      ISocket.SetOnMessageEvent( procedure (IO:IWebSocketIO;
                                 Message:TWebSocketMessageData)
        begin
          WriteLn(message.mdText);
        end);

      // Connect to our server
      ISocket.Connect('ws://192.168.38.108:8080',['void']);
    end;
  end;
end;

As you can see from the code above you are pretty much shielded from all the technical aspects of the WebSocket standard. You simply ask for the service and then use the service to create a websocket instance – which you access through the socket interface.

As a user you don’t have to care about the gory details here. You don’t have to worry about stuff like instance factories, if your code is running on FireFox, Chrome, MS10 or Safari. And even better is that you no longer have to care if it’s running on desktop, iOS, Android or an embedded system.

Here is the full IWebSocket interface

  IWebSocketIO = interface
    Procedure WriteString(const value:String);
    procedure WriteMemory(const Value:TMemoryHandle);
    Procedure WriteStream(const Value:TStream);
    Procedure WriteBinary(Const Data:TBinaryData);

    function  GetSocketState:TWebSocketState;
    function  GetConnected:Boolean;
    function  GetURL:String;
    function  GetProtocol:String;

    function  GetBinaryMessageDataType:TWebSocketBinaryDataType;
    procedure SetBinaryMessageDataType(const Value:TWebSocketBinaryDataType);

    procedure Disconnect;

    Property  BinaryMessageDataType:TWebSocketBinaryDataType
              read GetBinaryMessageDataType
              write SetBinaryMessageDataType;

    Property  SocketState:TWebSocketState read getSocketState;
    Property  Connected:Boolean read getConnected;
    property  URL:String read getURL;
    Property  Protocol:String read getProtocol;
  end;

  (* Websocket service interface *)
  IWebSocket = interface
    function  GetBinaryMessageDataType:TWebSocketBinaryDataType;
    procedure SetBinaryMessageDataType(const Value:TWebSocketBinaryDataType);

    function  GetSocketState:TWebSocketState;
    function  GetConnected:Boolean;
    function  GetURL:String;
    function  GetProtocol:String;

    procedure Connect(URL:String;Protocols:Array of String);
    procedure Disconnect;

    Procedure WriteString(const value:String);
    procedure WriteMemory(const Value:TMemoryHandle);
    Procedure WriteStream(const Value:TStream);
    Procedure WriteBinary(Const Data:TBinaryData);

    function  GetOnOpenEvent:TWebSocketOpenEvent;
    function  GetOnClosedEvent:TWebSocketCloseEvent;
    function  GetOnMessageEvent:TWebSocketMessageEvent;
    function  GetOnErrorEvent:TWebSocketErrorEvent;

    procedure SetOnOpenEvent(const Handler:TWebSocketOpenEvent);
    procedure SetOnClosedEvent(const Handler:TWebSocketCloseEvent);
    procedure SetOnMessageEvent(const Handler:TWebSocketMessageEvent);
    procedure SetOnErrorEvent(const Handler:TWebSocketErrorEvent);

    Property  SocketState:TWebSocketState read getSocketState;
    Property  Connected:Boolean read getConnected;
    property  URL:String read getURL;
    Property  Protocol:String read getProtocol;

    Function  GetServiceElement:IServiceElement;

    Property  BinaryMessageDataType:TWebSocketBinaryDataType
              read GetBinaryMessageDataType
              write SetBinaryMessageDataType;
    Property  OnOpen:TWebSocketOpenEvent read GetOnOpenEvent write SetOnOpenEvent;
    Property  OnClosed:TWebSocketCloseEvent read GetOnClosedEvent write SetOnClosedEvent;
    Property  OnMessage:TWebSocketMessageEvent read GetOnMessageEvent write SetOnMessageEvent;
    Property  OnError:TWebSocketErrorEvent read GetOnErrorEvent write SetOnErrorEvent;
  end;

Keep it simple, keep it safe

The benefit for us in terms of delivering a better Smart Mobile Studio, is that it allows us to compartmentalize our code. We get to create service classes for FireFox, Chrome and so on without having to go through hoops just to make it work identically everywhere. It works almost like a driver or plugin system, where the correct factory and service provider (sigh) is resolved at runtime. Most of the time we get away with a single implementation class, but it’s nice to know that we can isolate code for one browser in its own class, and support for the same features for another browser in another class. This helps keep the RTL sane and organized.

Platform services is pretty cool and saves you a lot of time

Platform services is pretty cool and saves you a lot of time

While no date has been set, I can at least tell you that this system will not be available until we reach at least version 2.3.x; at the very least.

Copas, Delphi script for Shell

October 21, 2015 11 comments

Have you ever wished that there was some way to script the shell without resorting to archaic batch files, python or any of the other weird languages out there? Well, a while back I started to implement a special version of DWScript (Delphi web script) which does exactly that. It allows you to execute DWScript files directly from the shell powered by a rich set of OS level functionality.

I havent come up with a cool name for it yet so i dubbed it copas, short for “command-line object pascal”. Since DWScript is the parser engine we use with Smart Mobile Studio, the dialect is identical – and you can even use some of the Smart Pascal classes with it (no web-engine stuff naturally).

It's a start

It’s a start

If someone updates the DWScript repository for FreePascal to the latest, we can kiss python, node.js and all the other odd-ball scripting engines for shell/bash goodbye!

Why is this cool?

Being able to execute pascal scripts is not the hard part (since that is already covered by DWScript), the hard part is providing classes that allows you to interact with the operative system. Things like file creation, pipes, executing applications, service rights — these are not complex but rather time-consuming tasks.

Since time is not something I have these days, it will take some time before I release any source-code for this, but perhaps over Xmas I’ll have time to finish it. I started on this months ago but havent had time to do much work on it. But streams and a pipe client (for windows) is already in place. The plan is to compile with FireDAC (or morpheus) so you can connect directly to all major DB engines, create databases on the fly, copy data between engines, perform backup tasks with ease — well, the possibilities are endless and only limited by time and imagination.

But the benefits should be obvious:

  • creating scripts that deals with files and folders can now be done in a language you know and love
  • Full OOP support, including partial classes, interfaces and all the cool stuff Smart Pascal is currently offering
  • Script servers (REST, WebSocket etc)
  • Add smart mobile studio and you have a fully object pascal driven infrastructure.
  • Perfect for automation tasks

Well, no time to complete this now – but after Xmas I’ll share out the SVN repo.

Writing a Delphi WebSocket server and Smart Mobile Client in 15 minutes

September 26, 2015 10 comments

Websocket is all the rage these days. In essence its just an extra layer built on top of ordinary HTTP (available as a snap-in for IIS and “mod” for apache). But Delphi developers like to build their solutions from the ground up! So what could possibly be better than to roll your own?

Indy to the rescue

No I didnt see you playing with your sockets SIR!

No I didnt see you playing with your sockets SIR!

Think of client-server programming and Delphi and chances are “indy” will be the first word to pop into your mind. It’s been there for ages, it’s rock solid, it supports every known RFC known to mankind – and it’s tried and tested by time. It may not provide the same speed as Microsoft Internet Explorer or Apache, but there are hundreds (if not thousands) of products out there built with the Indy library, so it’s pretty damn awesome!

But what about websocket? As far as standards go it’s the new kid on the block – invented more or less purely for secure HTML5/JavaScript development. Does Indy have that yet? Well, no. I’m sure it will be included at one point in a future update, but thankfully Indy is easy to extend and mold due to it’s purely object oriented nature.

A while back mr. Andre Mucche took the time to implement just that, extending an ordinary Indy HTTP server with the required plumbing – turning a bog standard, multi-threaded, multi-context HTTP server into a websocket nerdvana.

Why is this important?

If all you do is write old-school stuff in Delphi then you probably don’t need it, but if you want to keep up with the way technology is moving – then WebSockets is bound (pun intended) to cross your path sooner or later. If you havent already been asked by your customers, it’s only a matter of time before you are approached with the question “Can we poll data from our Delphi solution and use that on our website from JavaScript?”.

Well, there are many ways to deal with getting data from a Delphi centric solution (read: server) onto your website. You can spend weeks and months writing the JavaScript yourself, you can publish a few DataSnap API’s — or go for RemObjects SDK which IMHO is a much better alternative to DataSnap.

But Smart Mobile Studio offers an alternative route. The benefits should be fairly obvious:

  • You write object pascal (Delphi / FreePascal)
  • You don’t have to learn much JavaScript
  • All the low-level stuff is already wrapped and ready
  • Smart Mobile supports both RemObjects, DataSnap and Websocket (and a few more)

So how hard is it to create a Delphi websocket server and a Smart Mobile Studio client?

The Delphi side

Right, first start by creating a folder for your project. In my example I just named it “WebSocket”. Then create a fresh Delphi project (VCL) and save that into the folder as “SocketServer.dpr”.

Next, download Andre’s WebSocket extension units, these can be found here: https://github.com/andremussche/DelphiWebsockets. It’s Github so just download the zip archive. Once downloaded, unzip the files into your project folder. Your folder should look something like this by now:

Quick and dirty

Quick and dirty

With the files in place, add all the units to your project inside Delphi (including the superobject files). You dont really have to do this, you can unzip the files wherever you like — but for this quick demonstration I just stuff it all into the same project to avoid setting a path (it’s late, what can I say). Your Delphi project should now look like this:

Easy as apple-pie

Easy as apple-pie

With that in place, let’s add a TMemo component, a couple of buttons to control the server (start and stop) and isolate that in TActions. If you havent used actions before then please read up on that before you continue. It’s super simple and one of Delphi’s biggest strength’s over other RAD platforms out there. My form looks like this (just slap-dash 2 second stuff):

Not much to look at, but bling comes last

Not much to look at, but bling comes last

Now let’s write some code!

unit mainform;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants,
  System.Classes, Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs,

  IdComponent,
  IdContext,
  IdCustomHTTPServer,
  IdServerWebsocketContext,
  IdServerSocketIOHandling,
  IdWebsocketServer, Vcl.StdCtrls, System.Actions, Vcl.ActnList;

type
  TForm1 = class(TForm)
    Memo1: TMemo;
    Button1: TButton;
    Button2: TButton;
    ActionList1: TActionList;
    acStart: TAction;
    acStop: TAction;
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure acStartExecute(Sender: TObject);
    procedure acStopExecute(Sender: TObject);
    procedure acStartUpdate(Sender: TObject);
    procedure acStopUpdate(Sender: TObject);
  private
    { Private declarations }
    FServer:    TIdWebsocketServer;

    procedure   HandleServerStatus(ASender: TObject;
                const AStatus: TIdStatus;
                const AStatusText: string);

    procedure   HandleTextMessage(const AContext: TIdServerWSContext;
                const aText: string);

    procedure   HandleCommandGet(AContext: TIdContext;
                ARequestInfo: TIdHTTPRequestInfo;
                AResponseInfo: TIdHTTPResponseInfo);

  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.acStartExecute(Sender: TObject);
begin
  FServer.Active:=True;
end;

procedure TForm1.acStartUpdate(Sender: TObject);
begin
  TAction(sender).Enabled := not FServer.Active;
end;

procedure TForm1.acStopExecute(Sender: TObject);
begin
  FServer.Active := false;
end;

procedure TForm1.acStopUpdate(Sender: TObject);
begin
  TAction(sender).Enabled := FServer.Active;
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  FServer := TIdWebsocketServer.Create(NIL);
  FServer.OnStatus := HandleServerStatus;
  FServer.OnMessageText := HandleTextMessage;
  FServer.OnCommandGet := HandleCommandGet;
  FServer.KeepAlive := True;
  FServer.DefaultPort := 8080;
end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
  FServer.free;
end;

procedure TForm1.HandleCommandGet(AContext: TIdContext;
          ARequestInfo: TIdHTTPRequestInfo;
          AResponseInfo: TIdHTTPResponseInfo);
begin
  aResponseInfo.ContentText:='Hello world';
end;

procedure TForm1.HandleTextMessage(const AContext: TIdServerWSContext;
          const aText: string);
begin
  memo1.Lines.Add(aText);
end;

procedure TForm1.HandleServerStatus(ASender: TObject;
          const AStatus: TIdStatus;
          const AStatusText: string);
begin
  memo1.Lines.Add(aStatusText);
end;

end.

That’s basically it! The most bare-bone WebSocket server you will ever see. It just accepts a connection and dumps whatever text a client writes to the memo control on the form.
Right, now let’s look at the Smart Mobile Studio side of things.

The HTML5 Client

Fire up Smart Mobile Studio (im using the latest beta here) and create a new project. Remember to save the project before you start coding.
We will be adding a single button for connecting to the websocket server, and then a textbox for message input — and finally a “send” button to ship the next to the server.

This is what my slap-dash client looks like

This is what my slap-dash client looks like

With some components in place we move on to the WebSocket client code, which under the Smart Mobile RTL is a piece of cake:

unit Form1;

interface

uses 
  SmartCL.inet,
  SmartCL.System, SmartCL.Graphics, SmartCL.Components, SmartCL.Forms, 
  SmartCL.Fonts, SmartCL.Borders, SmartCL.Application, SmartCL.Controls.Button,
  SmartCL.Controls.Memo, SmartCL.Controls.EditBox;

type
  TForm1 = class(TW3Form)
    procedure W3Button2Click(Sender: TObject);
    procedure W3Button1Click(Sender: TObject);
  private
    {$I 'Form1:intf'}
    FSocket:  TW3WebSocket;
  protected
    procedure InitializeForm; override;
    procedure InitializeObject; override;
    procedure Resize; override;
  end;

implementation

{ TForm1 }

procedure TForm1.W3Button1Click(Sender: TObject);
begin
  try
    FSocket.Connect('ws://192.168.10.106:8080',[]);
  except
    on e: exception do
    showmessage(e.message);
  end;
end;

procedure TForm1.W3Button2Click(Sender: TObject);
var
  mText:String;
begin
  mText :=trim(w3Editbox1.text);

  if mtext.length > 0 then
    FSocket.Write(mText);

  w3Editbox1.text := '';
end;

procedure TForm1.InitializeForm;
begin
  inherited;
  // this is a good place to initialize components
  FSocket := TW3WebSocket.Create;
  FSocket.OnOpen := procedure (sender:TW3WebSocket)
    begin
      w3memo1.text := w3memo1.text + "WebSocket open" + #13;
    end;
end;

procedure TForm1.InitializeObject;
begin
  inherited;
  {$I 'Form1:impl'}
end;
 
procedure TForm1.Resize;
begin
  inherited;
end;
 
initialization
  Forms.RegisterForm({$I %FILE%}, TForm1);
end.

The final result

Now fire up your Delphi project, click the “start” button to initialize the server (note: the firewall may ask you to allow the server to use the port, remember to check “local networks” and just click “ok”). Your Delphi server should now run at port 8080 — use your favorite browser to check that it works. It should return “hello world” (see “HandleCommandGet” event handler in the Delphi code).

Next, fire up your Smart Mobile Studio project. Hit the “Connect” button, type something in the text-field and click “send” to ship it off to the server. Now watch the memo on the server and voila — you have just written your first websocket client/server system in less than 15 minutes!

Websocket has never been easier!

Websocket has never been easier!

Note: Remember to use your local IP. The IP listed in the SMS example above is just a local address on my local network. If you are running Delphi and SMS on the same machine, just use 127.0.0.1 and bob’s your uncle.

Enjoy!

Structures, dealing with name-pairs

September 26, 2015 Leave a comment

A small article on dealing with dynamically created, in-memory “records”. Head over to www.smartmobilestudio.com and have a peek 🙂

Smart Mobile Studio, update right around the corner

September 19, 2015 Leave a comment

So I havent been able to write much about Smart Mobile Studio the past six months. For those of you that read my blog you will notice that I made it quite clear that at least six months would pass before I could start to allocate time for blogging again. Between then and now I have moved house, got a new job and we’ve also had a long national holiday.

Doing it properly

On the surface things may appear to be slow, but behind the scenes the team has been working like mad. Not just with fixing bugs and beta-testing the new features, but also with establishing a proper company with proper funding and real-life shares. These are things you dont just slap together because it has a 1:1 impact on every single task, from customer support to technical growth. We love our product so much and we know from experience that it has, can and will make a huge impact on modern software development. These administrative topics are being finalized right now, which can only be regarded as happy news for everyone.

As for progress we like to keep things tidy, meaning that we stick to our bug reports as reported by customers and beta-testers. I get a lot of messages on facebook, emails and even the odd text-message about a bug or missing feature — but please note that we stick to fixing bugs that come in via the proper channels. And we have taken every single bug our now extended beta program has produced and done something about it. As with all living products there are always things, but rest assured that we are not resting on our laurels. And we are not going anywhere.

Next there is support and legacy work. If you have downloaded the beta release you may have noticed that Smart Mobile now gives you methods like allocmem, freemem, move, fillchar, reallocmem, streams (proper stuff, not just slap dash string manipulation), buffer classes and all the “missing” bits and pieces which makes the product stand out.

Javascript doesnt have this out of the box. So bringing things like pointers (references) and memory management into the RTL has been quite a challenge. Not just from a programming point of view, but also with regards to legacy browsers. A lot of people are still using older phones and not all of them supports UInt8 and UInt8Clamped datatypes. This is a pickle no doubt about it.

One of the first things we noticed on releasing the new RTL sub-layer, was in fact that UInt8ClampedArray only works on modern browsers, the absolute latest stuff (which I must admit was my fault because I’m a tech junky and always have the latest to play with). Older versions of IE, and even some variations of webkit for android lack this particular extension. And while I’m tempted to say “get a proper phone”, thats not how it works in real life. Its our job to try and be as compatible as possible. Which means time and effort.

To solve this I had to backtrack and make due with UInt8Array which is just as fast as the clamped version, but with a few limitations here and there – depending on how you use it. Thankfully my initial architecture of the RTL saved my bacon, because I’ve isolated “intrinsic conversion routines” (and reverse) in it’s own class, so we dont have clamped arrays all over the place to deal with. All it took was a few adjustments and we should now support a more rich set of mobile devices.

Having said that, altering the RTL to use a new type is not without it’s challenges; you still have to test, test and test again to make sure it works as expected. I sure as hell wont submit to “gimmic” solutions deployed by our competitors – namely stuffing bin/hex bytes in strings. It may be clever and it may work – but the price is speed, memory bloating and .. well its just not what Smart Mobile Studio is about.

Whats so important about old-time allocmem anyways?

Up to this point Smart Mobile Studio has been strides beyond typescript and other competitors technically. They have more components and things like that, but they also have a language which sounds exactly like if you insert something into, or indeed, take something out – of a cat. All those stupid curley-wurley brackets, lack of structure — and working with “real” data, like raw binary files and binary file-formats requires you to use esoteric libraries with tons of code. It’s ridicules to watch how they manage to get away with it, but kids with no knowledge of object pascal lap it up like catnip. Well we want something better!

Being able to write object oriented, fully memory capable applications is what makes FreePascal and Delphi such a joy to use for the desktop, and we want the same for nodeJS and the browser. Take something simple, like generating a PDF document on your nodeJS server. Should it really takes thousands of lines of code just to produce a binary file correctly? No. It should be no different than working with Delphi or FreePascal. Things like streams, encoding and decoding intrinsic types, working with arrays of bytes (and so on) should just work. But all such features requires a standard way to deal with memory: from allocating to reading and writing bytes, longwords or even bits !

Being able to allocate memory, scale memory, move chunks of memory around and ultimately read and write to the memory — this is the foundation on which streams and buffers rest. Without it we are left with “hacks” like we see other vendors provide. And since the browser exposes said functionality (albeit in an alien form compared to WinAPI) it’s imperative that we make use of it.

To give you some sense of what’s being written, here is one of 4 units dealing exclusively with the memory issue:

 

unit System.Memory;

interface

uses
  System.Types,
  System.TypeCon,
  W3C.TypedArray,
  SmartCL.System;

type

  TAllocation = Class;
  TAllocationOptions  = Class;

  (* TAllocation represents a single, managed memory allocation
     segment. It exposes the bare-bone information about the
     memory segment, such as length, buffer reference
     and handle. It also includes the most basic, simple low-level
     functions for scaling the memory, re-allocating the
     memory (with data persistence) and releasing the allocation *)
  EAllocation = Class(EW3Exception);
  TAllocation = Class(TObject,IDataTransport)
  private
    FHandle:    TMemoryHandle;
    FSize:      Integer;
    FOptions:   TAllocationOptions;

    (* IMPLEMENTS:: IDataTransport *)
    function    dataGetSize:Integer;
    function    dataRead(const Offset:Integer;
                const ByteCount:Integer):TByteArray;
    procedure   dataWrite(const Offset:Integer;
                const Bytes:TByteArray);
    function    dataOffset:Integer;
  protected
    Procedure   HandleAllocated;virtual;
    Procedure   HandleReleased;virtual;
    function    getTotalSize:Integer;virtual;
    function    getSize:Integer;virtual;
    function    getBufferHandle:TBufferHandle;virtual;
    function    getHandle:TMemoryHandle;virtual;
  public
    Property    Options:TAllocationOptions read FOptions;
    Property    Empty:Boolean read (not (FHandle));
    property    Allocated:Boolean read ( (FHandle) );
    property    Handle:TMemoryHandle read getHandle;
    Property    Buffer:TBufferHandle read getBufferHandle;
    Property    Size:Integer read getSize;
    Property    TotalSize:Integer read getTotalSize;

    Procedure   Transport(const Target:IDataTransport);

    procedure   Allocate(Bytes:Integer);
    procedure   Release;
    procedure   Grow(const Bytes:Integer);
    procedure   Shrink(const Bytes:Integer);
    procedure   ReAllocate(Const NewSize:Integer);
    Constructor Create;virtual;
    Destructor  Destroy;Override;
  end;

  (*  TAllocationOptions defined caching options for TAllocation.
      Caching means that TAllocation will always allocate extra data which
      it uses for faster growth. So if you allocate 45 bytes and have a
      cache-size of 1024, the number of actual bytes allocated will be 1069.
      Once allocated, growth will take memory from the cache rather than
      re-allocate memory directly, which is much faster.
      After the initial allocation, only when the cache is exhausted will
      another allocation be performed *)
  TAllocationOptions = Class(TW3OwnedObject)
  private
    FUseCache:  Boolean;
    FCacheSize: Integer;
  protected
    Procedure setUseCache(const Value:Boolean);
    procedure setCacheSize(value:Integer);
    function  getCacheUsed:Integer;
    function  getCacheFree:Integer;
  public
    property    Owner: TAllocation read ( TAllocation(Inherited Owner) );
    Property    UseCache:Boolean read FUseCache write setuseCache;
    Property    CacheSize:Integer read FCacheSize write setCacheSize;
    Property    CacheUsed:Integer read getCacheUsed;
    Property    CacheFree:Integer read getCacheFree;
    constructor Create(AOwner: TAllocation); reintroduce;
  end;

  (* TAddress is a marshaled pointer type. JavaScript does not support
     pointers out of the box, so when dealing with memory allocations
     and offsets into such a buffer, which is what a pointer in essence
     represents -- you can replace FreePascal/Delphi pointer types with
     a marshaled pointer.

     Where you under FreePascal or Delphi would write:

        Move(Source^,target^,Size);

     Smart Pascal now supports:

        TMarshal.Move(TAddressSource.Create(FSource,0),
          TAddressTarget.Create(FTarget,0),FSource.Size);

     TMarshal supports variations of typical memory operations, both
     on the level of handle (TMemoryHandle) and TAddress pointers. *)
  EAddress  = Class(EW3Exception);
  TAddress  = partial class(TObject)
  private
    FOffset:    Integer;
    FBuffer:    TMemoryHandle;
  public
    Property    Entrypoint:Integer read FOffset;
    Property    Segment:TMemoryHandle read FBuffer;
    Property    Size:Integer read ( JArrayBuffer(FBuffer).byteLength );
    function    Addr(const Index:Integer):TAddress;
    Constructor Create(const aSegment:TMemoryHandle;
                const aEntrypoint:Integer);overload;virtual;
    Destructor  Destroy;Override;
  end;

  (* TUnmanaged is a static class with a collection of un-managed
     (non marshaled pointers) functions for allocating and accessing
     memory allocations. These are best suited for advanced users *)
  TUnManaged = Class static
  public
    class function  AllocMemA(const Size:Integer):TMemoryHandle;

    class procedure FreeMemA(const Memory:TMemoryHandle);

    class function  ReAllocMemA(Memory:TMemoryHandle;
                    Size:Integer):TMemoryHandle;

    class function  ReadMemoryA(const Memory:TMemoryHandle;
                    const Offset:Integer;
                    Size:Integer):TMemoryHandle;overload;

    class function  WriteMemoryA(const Memory:TMemoryHandle;
                    const Offset:Integer;
                    const Data:TMemoryHandle):Integer;

    class procedure FillMemoryA(const Memory:TMemoryHandle;
                    const Offset:Integer;
                    Size:Integer;
                    const Data:TMemoryHandle);
  end;

  (* TMarshal is a class which contains methods for allocating
     managed (marshaled) pointers, moving data quickly between memory
     segments - and also it provides some legacy methods from native
     object pascal, such as Move(), FillChar(), AllocMem() and FreeMem() *)
  TMarshal = class static
  public
    class property  UnManaged:TUnManaged;
    class function  AllocMem(const Size:Integer):TAddress;
    class procedure FreeMem(Const Segment:TAddress);

    class procedure Move(const Source:TAddress;
                    const Target:TAddress;
                    const Size:Integer);overload;

    class procedure Move(const Source:TMemoryHandle;
                    const SourceStart:Integer;
                    const Target:TMemoryHandle;
                    const TargetStart:Integer;
                    const Size:Integer);overload;

    class Procedure FillChar(const Target:TAddress;
                    const Size:Integer;
                    const Value:String);overload;

    class procedure FillChar(const Target:TAddress;
                    const Size:Integer;
                    const Value:Byte);overload;

    class procedure ReAllocMem(var Segment:TAddress;
                    const Size:Integer);

    class function  ReadMemory(const Segment:TAddress;
                    const Size:Integer):TByteArray;overload;

    class procedure WriteMemory(const Segment:TAddress;
                    const Data:TByteArray);

    class procedure Fill(Const Buffer:TMemoryHandle;Offset:Integer;
                    ByteLen:Integer;const Value:Byte);
  end;

implementation

//############################################################################
// TAllocationOptions
//############################################################################

constructor TAllocationOptions.Create(AOwner: TAllocation);
begin
  inherited Create(AOwner);
  FCacheSize:=4096;
  FUseCache:=true;
end;

function TAllocationOptions.getCacheFree:Integer;
begin
  result:=FCacheSize - getCacheUsed;
end;

function TAllocationOptions.getCacheUsed:Integer;
begin
  if FUseCache then
  result:=FCacheSize - (owner.Handle.length - owner.Size) else
  result:=0;
end;

Procedure TAllocationOptions.setUseCache(const Value:Boolean);
begin
  FUseCache:=Value;
end;

procedure TAllocationOptions.setCacheSize(value:Integer);
begin
  FCacheSize:=TInteger.EnsureRange(Value,1024,1024 * 1000);
end;

//############################################################################
// TAllocation
//############################################################################

Constructor TAllocation.Create;
Begin
  inherited Create;
  FOptions:=TAllocationOptions.Create(self);
end;

Destructor TAllocation.Destroy;
begin
  if (FHandle) then
  Release;
  FOptions.free;
  inherited;
end;

Procedure TAllocation.Transport(const Target:IDataTransport);
var
  mOffset:  Integer;
begin
  if assigned(target) then
  begin
    if not Empty then
    begin
      try
        mOffset:=Target.dataOffset;
        Target.dataWrite(mOffset,
        TDataType.TypedArrayToBytes(TW3DefaultBufferType(Handle)));
      except
        on e: exception do
        Raise EAllocation.CreateFmt
        ('Data transport failed, mechanism threw exception %s with error [%s]',
        [e.classname,e.message]);
      end;
    end;
  end else
  Raise EAllocation.Create
  ('Invalid transport interface, reference was NIL error');
end;

// INTERFACE: IDataTransport
function TAllocation.dataOffset:Integer;
begin
  result:=0;
end;

// INTERFACE: IDataTransport
function TAllocation.dataGetSize:Integer;
Begin
  result:=getSize;
end;

// INTERFACE: IDataTransport
function TAllocation.dataRead(const Offset:Integer;
         const ByteCount:Integer):TByteArray;
var
  mRef: TMemoryHandle;
begin
  mRef:=TUnManaged.ReadMemoryA(Handle,Offset,ByteCount);
  result:=TDatatype.TypedArrayToBytes(TW3DefaultBufferType(mRef));
end;

// INTERFACE: IDataTransport
procedure TAllocation.dataWrite(const Offset:Integer;
          const Bytes:TByteArray);
begin
  TUnManaged.WriteMemoryA(Handle,Offset,TDataType.BytesToTypedArray(Bytes));
end;

Procedure TAllocation.HandleAllocated;
begin
  // Decendants should override this method
end;

Procedure TAllocation.HandleReleased;
begin
  // Decendants should override this method
end;

procedure TAllocation.Allocate(Bytes:Integer);
var
  mSize:  Integer;
begin
  if (FHandle) then
  Release;

  if Bytes>0 then
  begin
    (* Round off to nearest factor of 16. This is required when
       working with pixel-buffers. And also faster to allocate *)
    mSize:=TInteger.ToNearest(Bytes,16);

    (* Allocate with cache? *)
    if FOptions.UseCache then
    inc(mSize,FOptions.CacheSize);

    FHandle:=TUnManaged.AllocMemA(mSize);
    FSize:=Bytes;
    HandleAllocated;
  end;
end;

procedure TAllocation.Release;
begin
  if (FHandle) then
  Begin
    FHandle.buffer:=NIL;
    FHandle:=null;
    FSize:=0;
    HandleReleased;
  end;
end;

procedure TAllocation.Grow(const Bytes:Integer);
begin
  if (FHandle) then
  Begin
    if FOptions.UseCache then
    begin
      if bytes < FOptions.CacheFree then begin inc(FSize,Bytes); exit; end else begin (* Cache exhausted, re-allocate *) ReAllocate(FSize+ Bytes); end; exit; end; (* No cache is used, simply grow the buffer *) inc(FSize,bytes); ReAllocate(FSize); end else allocate(bytes); end; procedure TAllocation.ReAllocate(const NewSize:Integer); var mSize: Integer; begin if (FHandle) then begin HandleReleased; (* Size in bytes *) mSize:=newSize; (* Allocate cache? *) if FOptions.UseCache then inc(mSize,FOptions.CacheSize); (* Re-allocate memory *) FHandle:=TUnManaged.ReAllocMemA(FHandle,mSize); (* Define size MINUS cache *) FSize:=newSize; end else Allocate(newSize); HandleAllocated; end; procedure TAllocation.Shrink(const Bytes:Integer); var mSize: Integer; begin if (FHandle) then begin (* Use memory caching? *) if FOptions.UseCache then begin (* How many bytes are left after scale? *) mSize:=TInteger.EnsureRange(getSize - bytes,0,MAX_INT); (* Data left to work with? *) if mSize>0 then
      begin

        (* More than defined-size PLUS cache? *)
        if mSize>(FSize + FOptions.CacheSize) then
        Begin
          (* Scale down to defined size, this retails data
             and also include the cache size *)
          ReAllocate(mSize);
        end else
        begin
          (* The data released is within the bounds of the
             defined cache size, so we simply adjust the exposed size *)
          FSize:=mSize;
        end;
      end else
      release;
      exit;
    end;

    mSize:=TInteger.EnsureRange(getSize - bytes,0,MAX_INT);
    if mSize>0 then
    ReAllocate(mSize) else
    Release;
  end;
end;

function TAllocation.getTotalSize:Integer;
begin
  if (FHandle) then
  result:=FHandle.length;
end;

function TAllocation.getSize:Integer;
begin
  result:=FSize;
end;

function TAllocation.getHandle:TMemoryHandle;
begin
  result:=FHandle;
end;

function TAllocation.getBufferHandle:TBufferHandle;
begin
  if (FHandle) then
  result:=FHandle.buffer else
  result:=null;
end;

//############################################################################
// TAbsolute
//############################################################################

class function TUnManaged.AllocMemA(const Size:Integer):TMemoryHandle;
begin
  if Size>0 then
  Result:=new TW3DefaultBufferType(Size) else
  result:=null;
end;

class procedure TUnManaged.FreememA(const Memory:TMemoryHandle);
begin
  if (memory) then
  begin
    // decouple buffer from type
    // this does not release memory, but "hints" to the GC
    // to mark the segment for level 1 release classification
    TW3DefaultBufferType(Memory).buffer := NIL;
  end;
end;

class function TUnManaged.ReAllocMemA(Memory:TMemoryHandle;
         Size:Integer):TMemoryHandle;
begin
  if (Memory) then
  begin
    if Size>0 then
    begin
      result:=new TW3DefaultBufferType(Size);
      TMarshal.Move(Memory,0,result,0,Size);
    end;
  end else
  result:=AllocMemA(Size);
end;

class procedure TUnManaged.FillMemoryA(const Memory:TMemoryHandle;
                const Offset:Integer;
                Size:Integer;
                const Data:TMemoryHandle);
var
  x:  Integer;
  mToWrite: Integer;
  mEnd: Integer;
begin
  if (memory) then
  Begin
    if offset>=0 then
    begin
      if offset<memory.length then
      begin
        if (data) then
        begin

          x:=offset;
          mEnd:=offset + Size-1;

          while x<mEnd do begin mToWrite:=Data.length; if (x + mToWrite-1) > mEnd then
            mToWrite:=(x + mToWrite-1) - mEnd;
            if mToWrite<1 then break; TMarshal.Move(Data,0,Memory,x,mToWrite); inc(x,mToWrite); end; end; end; end; end; end; class function TUnManaged.WriteMemoryA(const Memory:TMemoryHandle; const Offset:Integer; const Data:TMemoryHandle):Integer; var mTotal: Integer; begin if (Memory) then begin if (Data) then begin mTotal:=offset + data.length; if mTotal > memory.length then
      result:=memory.length-mTotal else
      result:=data.length;

      if result>0 then
      begin
        if offset + data.length <=memory.length then TW3DefaultBufferType(Memory).Set(JTypedArray(data),offset) else begin (* Copy range from source, this results in a new buffer *) var mChunk:=TW3DefaultBufferType(data).buffer.Slice(0,result-1); (* Create a typed array pointing to buffer *) var mTemp := new TW3DefaultBufferType( JTypedArray(mChunk) ); (* write memory from source to target *) TW3DefaultBufferType(Memory).Set(mTemp,offset); end; end; end; end; end; class function TUnManaged.ReadMemoryA(const Memory:TMemoryHandle; const Offset:Integer; Size:Integer):TMemoryHandle; var mTotal: Integer; begin if (Memory) then begin if Offset>=0 then
    begin
      mTotal:=offset + Size;
      if mTotal > memory.length then
      Size:=memory.length-mTotal;

      if Size>0 then
      result:=new TW3DefaultBufferType(JTypedArray(
      TW3DefaultBufferType(Memory).buffer.Slice(Offset,Size)));
    end;
  end;
end;

//############################################################################
// TMarshal
//############################################################################

class function TMarshal.ReadMemory(const Segment:TAddress;
               const Size:Integer):TByteArray;
var
  x:  Integer;
  mOffset:  Integer;
  mLongs: Integer;
  mHandle:  TW3DefaultBufferType;
Begin
  if  (segment<>NIL)
  and (size>0) then
  begin
    mHandle:=TW3DefaultBufferType( Segment.Segment );
    mOffset:=Segment.Entrypoint;

    mLongs:=Size shr 3;
    x:=0;
    while mLongs>0 do
    begin
      result.add ( mHandle[mOffset + x] ); inc(x);
      result.add ( mHandle[mOffset + x] ); inc(x);
      result.add ( mHandle[mOffset + x] ); inc(x);
      result.add ( mHandle[mOffset + x] ); inc(x);
      result.add ( mHandle[mOffset + x] ); inc(x);
      result.add ( mHandle[mOffset + x] ); inc(x);
      result.add ( mHandle[mOffset + x] ); inc(x);
      result.add ( mHandle[mOffset + x] ); inc(x);
      dec(mLongs);
    end;

    case Size mod 8 of
    1:  result.add ( mHandle[mOffset + x] );
    2:  begin
        result.add ( mHandle[mOffset + x] ); inc(x);
        result.add ( mHandle[mOffset + x] );
        end;
    3:  begin
        result.add ( mHandle[mOffset + x] ); inc(x);
        result.add ( mHandle[mOffset + x] ); inc(x);
        result.add ( mHandle[mOffset + x] );
        end;
    4:  begin
        result.add ( mHandle[mOffset + x] ); inc(x);
        result.add ( mHandle[mOffset + x] ); inc(x);
        result.add ( mHandle[mOffset + x] ); inc(x);
        result.add ( mHandle[mOffset + x] );
        end;
    5:  begin
        result.add ( mHandle[mOffset + x] ); inc(x);
        result.add ( mHandle[mOffset + x] ); inc(x);
        result.add ( mHandle[mOffset + x] ); inc(x);
        result.add ( mHandle[mOffset + x] ); inc(x);
        result.add ( mHandle[mOffset + x] );
        end;
    6:  begin
        result.add ( mHandle[mOffset + x] ); inc(x);
        result.add ( mHandle[mOffset + x] ); inc(x);
        result.add ( mHandle[mOffset + x] ); inc(x);
        result.add ( mHandle[mOffset + x] ); inc(x);
        result.add ( mHandle[mOffset + x] ); inc(x);
        result.add ( mHandle[mOffset + x] );
        end;
    7:  begin
        result.add ( mHandle[mOffset + x] ); inc(x);
        result.add ( mHandle[mOffset + x] ); inc(x);
        result.add ( mHandle[mOffset + x] ); inc(x);
        result.add ( mHandle[mOffset + x] ); inc(x);
        result.add ( mHandle[mOffset + x] ); inc(x);
        result.add ( mHandle[mOffset + x] ); inc(x);
        result.add ( mHandle[mOffset + x] );
        end;
    end;
  end;
end;

class procedure TMarshal.WriteMemory(const Segment:TAddress;
                const Data:TByteArray);
begin
  if  (Segment<>NIL)
  and (data.length>0) then
  JIntegerTypedArray(segment.Segment).Set(Data,segment.Entrypoint);
end;

class procedure TMarshal.FillChar(const Target:TAddress;
      const Size:Integer;
      const Value:Byte);
var
  mSegment: TW3DefaultBufferType;
  mIndex:   Integer;
Begin
  if Target<>NIl then
  begin
    mSegment:=TW3DefaultBufferType( Target.Segment );
    if VarIsValidRef(mSegment) then
    Begin
      mIndex:=Target.Entrypoint;
      TMarshal.Fill(Target.Segment,mIndex,Size,Value);
    end;
  end;
end;

class procedure TMarshal.Fill(Const Buffer:TMemoryHandle;Offset:Integer;
      ByteLen:Integer;const Value:Byte);
var
  mTotalSize: Integer;
  mTarget:    JDataView;
  mTemp:      TMemoryHandle;
  mLongs:     Integer;
  x:          Integer;
  mLongword:  Integer;
Begin
  if (buffer) then
  begin

    mTotalSize:=TW3DefaultBufferType(Buffer).byteLength;

    if  ( offset >=0 )
    and ( offset < mTotalSize) then begin (* clip the offset so we dont overload the buffer *) if offset + ByteLen > TW3DefaultBufferType(Buffer).byteLength then
      ByteLen:=mTotalSize - Offset;

      mTemp:=TUnManaged.AllocMemA(4);
      try
        (* Populate a longword with 4 bytes, so we can fill a longword
           for each write rather than a single byte *)
        mTemp[0]:=Value;
        mTemp[1]:=Value;
        mTemp[2]:=Value;
        mTemp[3]:=Value;


        (* Cache our 4-byte longword *)
        mLongword:=TDatatype.TypedArrayToUInt32(TW3DefaultBufferType(mTemp));

        (* setup a dataview for the target *)
        asm
          @mTarget = new DataView((@Buffer).buffer);
        end;

        (* We will be setting 4 bytes per write, 32 bytes per loop *)
        x:=Offset;
        mLongs:=ByteLen shr 5;

        while mLongs>0 do
        begin
          mTarget.setUint32(x,mLongword,CNT_LittleEndian);inc(x,4);
          mTarget.setUint32(x,mLongword,CNT_LittleEndian);inc(x,4);
          mTarget.setUint32(x,mLongword,CNT_LittleEndian);inc(x,4);
          mTarget.setUint32(x,mLongword,CNT_LittleEndian);inc(x,4);
          mTarget.setUint32(x,mLongword,CNT_LittleEndian);inc(x,4);
          mTarget.setUint32(x,mLongword,CNT_LittleEndian);inc(x,4);
          mTarget.setUint32(x,mLongword,CNT_LittleEndian);inc(x,4);
          mTarget.setUint32(x,mLongword,CNT_LittleEndian);inc(x,4);
          dec(mLongs);
        end;

        mLongs:=ByteLen mod 32;
        while mLongs>0 do
        begin
          mTarget.setUint8(x,Value);inc(x);
          dec(mLongs);
        end;

      finally
        mTemp.free;
      end;
    end;
  end;
end;

class Procedure TMarshal.FillChar(const Target:TAddress;
      const Size:Integer;
      const Value:String);
var
  mSegment: TW3DefaultBufferType;
  mByte:    Byte;
Begin
  if Target<>NIl then
  begin
    if Value.length>0 then
    begin
      mByte:=TDataType.CharToByte(Value);
      mSegment:=TW3DefaultBufferType( Target.Segment );
      if VarIsValidRef(mSegment) then
      Fill(Target.Segment,Target.Entrypoint,Size, mByte);
    end;
  end;
end;

class procedure TMarshal.Move( const Source:TMemoryHandle;
                          const SourceStart:Integer;
                          const Target:TMemoryHandle;
                          const TargetStart:Integer;
                          const Size:Integer);
var
  mRef:TW3DefaultBufferType;
Begin
  if  Source.valid
  and (SourceStart>=0)
  and Target.valid
  and (TargetStart>=0)
  and (Size>0) then
  begin
    (* Copy memory to move into sub-array *)
    mRef:=TW3DefaultBufferType(Source).SubArray(SourceStart,SourceStart+Size);

    (* Write memory to buffer *)
    TW3DefaultBufferType(Target).Set(mRef,TargetStart);
  end;
end;

class procedure TMarshal.Move(const Source:TAddress;
          const Target:TAddress;const Size:Integer);
Begin
  if Source<>NIL then
  Begin
    if Target<>NIl then
    begin
      if Size>0 then
      Move(Source.segment,Source.Entrypoint,
      target.segment,target.entrypoint,Size);
    end;
  end;
end;

class procedure TMarshal.ReAllocmem(var Segment:TAddress;
                const Size:Integer);
var
  mTemp:  TAddress;
  mSize:  Integer;
begin
  if segment<>NIL then
  begin
    mSize:=TW3DefaultBufferType(segment.Segment).length;

    mTemp:=AllocMem(Size);

    case (Size>mSize) of
    true:   move(segment,mtemp,mSize);
    false:  move(segment,mTemp,Size);
    end;

    SegMent.free;
    Segment:=NIL;

    Segment:=mTemp;
  end else
  SegMent:=AllocMem(Size);
end;

class function TMarshal.AllocMem(Const Size:Integer):TAddress;
var
  mBuffer:  JArrayBuffer;
  mArray:   TW3DefaultBufferType;
begin
  result:=NIL;
  if Size>0 then
  Begin
    mBuffer :=  new JArrayBuffer(Size);
    mArray  :=  new TW3DefaultBufferType(mBuffer,0,Size);
    result  :=  TAddress.Create(mArray,0);
  end;
end;

class procedure TMarshal.FreeMem(Const Segment:TAddress);
begin
  if Segment<>NIL then
  Segment.free;
end;

//############################################################################
// TAddress
//############################################################################

Constructor TAddress.Create(const aSegment:TMemoryHandle;
            const aEntrypoint:Integer);
begin
  inherited Create;
  if aSegment.defined
  and aSegment.valid then
  FBuffer:=aSegment else
  Raise EAddress.Create('Failed to derive address, invalid segment error');

  if aEntryPoint>=0 then
  FOffset:=aEntryPoint else
  Raise EAddress.Create('Failed to derive address, invalid entrypoint error');
end;

Destructor TAddress.Destroy;
begin
  FBuffer:=NIL;
  FOffset:=0;
  inherited;
end;

function TAddress.Addr(const Index:Integer):TAddress;
var
  mTarget:  Integer;
begin
  if Index >= 0 then
  Begin
    mTarget:=FOffset + Index;
    if (mTarget>=0) and (mTarget < TW3DefaultBufferType(FBuffer).byteLength) then
    result:=TAddress.Create(FBuffer,mTarget) else
    raise EAddress.Create
    ('Failed to derive address, entrypoint exceeds segment bounds error');
  end else
  Raise EAddress.Create
  ('Failed to derive address, invalid entrypoint error');
end;


end.

Angular + knockout + bootstrap = Smart Mobile

May 5, 2015 8 comments

I have been suuper busy lately with work so I havent been able to upload the latest “angular, knockout, bootstrap KILLER” library.

In essence it goes like this:

  • Bootstrap is good for UI’s
  • Angular i good for data binding
  • Knockout is the same, but with a few cool twists

Smart mobile studio absorbs them all through object orientation, which in turn renders them useless compared to our model 🙂

Stay tuned for more eh.. knockout code 😀

Oh this is going to be fun :)

Oh this is going to be fun 🙂

CSS Builder Class, Smart Syntax

April 12, 2015 Leave a comment

In my last post I demonstrated just how flexible CSS can be when you start to automate it. Well, if you want to play around with this NOW as opposed to waiting for the next update, I have written down directions to do so here.

First you need the CSS class. This can be found in the QTX library which is hosted on Google Code (Click here to view the repository). Note: You dont need the whole library, just download the unit in the link and rename it and you’re good. Save the files in the libraries folder of Smart Mobile Studio (see the start button registration for Smart, there is a link to the RTL and Library folder).

Generating cool effects is now super easy

Generating cool effects is now super easy

Right. With the CSS class in your possession you now need the builder class. This is essentially a small class with keywords that, when used, generate CSS code according to the functions and when they are used. So if you do something wrong the CSS will come out wrong.

Here is the generator class so far:

unit darth.stylesheet;

//#############################################################################
//
//  DARTH COMPONENTS
//
//  Author:     Jon Lennart Aasenden
//  Copyright:  Jon Lennart Aasenden, all rights reserved
//
//#############################################################################

interface

uses
  System.Types,
  System.Colors,
  SmartCL.System;

type

  TDarthStyleSheet = Class(TObject)
  private
    FHandle:    THandle;
  protected
    function    getSheet:THandle;
    function    getRules:THandle;
    function    getCount:Integer;
    function    getItem(index:Integer):String;
  public
    Property    Sheet:THandle read getSheet;
    Property    Handle:THandle read FHandle;
    function    Add(aName:String;const aRules:String):String;overload;
    Procedure   Add(const aRuleText:String);overload;

    Property    Count:Integer read getCount;
    Property    Items[index:Integer]:String
                read getItem;

    class procedure addClassToElement(const aElement:THandle;const aName:String);
    class procedure removeClassFromElement(const aElement:THandle;const aName:String);
    class function  findClassInElement(const aElement:THandle;const aName:String):Boolean;

    Constructor Create;virtual;
    Destructor  Destroy;Override;
  End;


  TSuperStyle = static class
  public
    class function  EdgeRound(Size:Integer):String;overload;
    class function  EdgeRound(topleftP,toprightP,bottomleftP,
                    bottomrightP:Integer):String;overload;
    class function  EdgeTopaz:String;
    class function  EdgeAngaro:String;

    class function  AnimGlow(aFrom,aTo:TColor):String;

    class procedure AnimStart(Handle:TControlHandle;animName:String);

  end;


implementation

var
_sheet: TDarthStyleSheet;

function getStyleSheet:TDarthStyleSheet;
begin
  if _sheet=NIL then
  _sheet:=TDarthStyleSheet.Create;
  result:=_sheet;
end;

class procedure TSuperStyle.AnimStart(Handle:TControlHandle;animName:String);
begin
  animName:=animName.trim;
  if  (animname.length>0)
  and (handle) then
  begin
    Handle.style['-webkit-animation-name']:=animName;
    Handle.style['-webkit-animation-duration']:='2s';
    Handle.style['-webkit-animation-iteration-count']:='infinite';
  end;
end;

  type
  TCSS = class
  public
    Property  Text:String;
    function  KeyFrames:TCSS;
    function  From:TCSS;
    function  &To:TCSS;
    function  Enter:TCSS;
    function  Leave:TCSS;
    function  PercentOf(Value:Integer):TCSS;
    function  IntOf(Value:Integer):TCSS;
    function  ColorOf(Value:TColor):TCSS;
    function  &inc(Value:String):TCSS;
    function  CRLF:TCSS;
    function  OpenParam:TCSS;
    function  CloseParam:TCSS;
    function  Background:TCSS;
    function  BoxShadow(Left,Top,Right,Bottom:Integer):TCSS;overload;
    function  BoxShadow(Left,Top,Right,Bottom:Integer;
              Color:TColor):TCSS;overload;

    function  LinearGradientV(aFrom,aTo:TColor):TCSS;
    function  LinearGradientH(aFrom,aTo:TColor):TCSS;
    function  LinearGradientTL(aFrom,aTo:TColor):TCSS;
    function  LinearGradientTR(aFrom,aTo:TColor):TCSS;

    Function  BeginComplexGradient(Angle:Integer):TCSS;
    function  EndComplexGradient:TCSS;

    function  ColorPercent(PercentOf:Integer;Color:TColor):TCSS;

    function  LinearGradientAngle(aFrom,aTo:TColor;
              Const Angle:Float):TCSS;overload;

    function  LinearGradientAngle(Colors:Array of TColor;
              Const Angle:Float):TCSS;overload;

    function  AnimationName(name:String):TCSS;
    function  AnimationDuration(Secs,MSecs:Integer):TCSS;
    function  AnimationInfinite:TCSS;
  end;

function TCSS.KeyFrames:TCSS;
begin
  Text:=Text + '@-webkit-keyframes ';
  result:=self;
end;

function TCSS.CRLF:TCSS;
begin
  Text:=Text + #13;
  result:=self;
end;

function  TCSS.OpenParam:TCSS;
begin
  Text:=Text + ' (';
  result:=self;
end;

function  TCSS.CloseParam:TCSS;
begin
  Text:=Text + ') ';
  result:=self;
end;

function TCSS.&inc(Value:String):TCSS;
begin
  Text:=Text + value;
  result:=self;
end;

function TCSS.PercentOf(Value:Integer):TCSS;
begin
  Text:=Text + TInteger.EnsureRange(Value,0,100).toString + '% ';
  result:=self;
end;

function TCSS.IntOf(Value:Integer):TCSS;
begin
  Text:=Text + Value.toString + ' ';
  result:=self;
end;

function TCSS.ColorOf(Value:TColor):TCSS;
begin
  Text:=Text + ColorToStr(Value) + ' ';
  result:=self;
end;

function  TCSS.Enter:TCSS;
begin
  Text:=Text + '{' + #13#10;
  result:=self;
end;

function  TCSS.Leave:TCSS;
begin
  Text:=Text + '}' + #13#10;
  result:=self;
end;

function TCSS.From:TCSS;
begin
  Text:=Text + 'from ';
  result:=self;
end;

function TCSS.&To:TCSS;
begin
  Text:=Text + 'to ';
  result:=self;
end;

function TCSS.BoxShadow(Left,Top,Right,Bottom:Integer):TCSS;
begin
  Text:=Text + '-webkit-box-shadow: '
    + TInteger.ToPxStr(Left) + ' '
    + TInteger.ToPxStr(Top) + ' '
    + TInteger.ToPxStr(Right) + ' '
    + TInteger.ToPxStr(Bottom) + ';';
  result:=self;
end;

function TCSS.BoxShadow(Left,Top,Right,Bottom:Integer;Color:TColor):TCSS;
begin
  Text:=Text + '-webkit-box-shadow: '
    + TInteger.ToPxStr(Left) + ' '
    + TInteger.ToPxStr(Top) + ' '
    + TInteger.ToPxStr(Right) + ' '
    + TInteger.ToPxStr(Bottom) + ' '
    + ColorToWebStr(Color) + '; ';
  result:=self;
end;

function TCSS.Background:TCSS;
begin
  Text:=Text +'background:';
  result:=Self;
end;

function  TCSS.LinearGradientV(aFrom,aTo:TColor):TCSS;
begin
  Text:=Text + '-webkit-linear-gradient('
  + ColorToWebStr(aFrom) + ',' + ColorToWebStr(aTo) + ');';
  result:=Self;
end;

function  TCSS.LinearGradientH(aFrom,aTo:TColor):TCSS;
begin
  Text:=Text + '-webkit-linear-gradient(left,'
  + ColorToWebStr(aFrom) + ',' + ColorToWebStr(aTo) + ');';
  result:=Self;
end;

function TCSS.LinearGradientTL(aFrom,aTo:TColor):TCSS;
begin
  Text:=Text + '-webkit-linear-gradient(left top,'
  + ColorToWebStr(aFrom) + ',' + ColorToWebStr(aTo) + ');';
  result:=Self;
end;

function TCSS.LinearGradientTR(aFrom,aTo:TColor):TCSS;
begin
  Text:=Text + '-webkit-linear-gradient(right top,'
  + ColorToWebStr(aFrom) + ',' + ColorToWebStr(aTo) + ');';
  result:=Self;
end;

function TCSS.LinearGradientAngle(aFrom,aTo:TColor;Const Angle:Float):TCSS;
begin
  Text:=Text + '-webkit-linear-gradient(' + FloatToStr(Angle)
    + 'deg, ' + ColorToWebStr(aFrom) + ',' + ColorToWebStr(aTo) + ');';
  result:=Self;
end;

function TCSS.LinearGradientAngle(Colors:Array of TColor;Const Angle:Float):TCSS;
var
  x:  Integer;
begin
  Text:=Text + '-webkit-linear-gradient('
  + FloatToStr(Angle) + 'deg, ';
  for x:=0 to Colors.length-1 do
  begin
    Text:=Text + ColorToWebStr(Colors[x]);
    if x<Colors.length-1 then
    Text:=Text + ',';
  end;
  Text:=Text + ');';
  result:=Self;
end;

Function TCSS.BeginComplexGradient(Angle:Integer):TCSS;
begin
  Text:=Text + '-webkit-linear-gradient(' + FloatToStr(Angle) + 'deg,';
  result:=self;
end;

function TCSS.EndComplexGradient:TCSS;
begin
  Text:=Text + ');';
  result:=self;
end;

function TCSS.ColorPercent(PercentOf:Integer;Color:TColor):TCSS;
begin
  if Text[length(text)]='%' then
  Text:=Text + ',';

  Text:=Text + ColorToWebStr(Color) +  ' '
  + TInteger.EnsureRange(PercentOf,0,100).toString + '%';
  result:=self;
end;

function TCSS.AnimationName(name:String):TCSS;
begin
  Text:=Text + '-webkit-animation-name: ' + Name + ';' + #13#10;
  result:=self;
end;

function TCSS.AnimationDuration(Secs,MSecs:Integer):TCSS;
begin
  Text:=text + '-webkit-animation-duration: ' + IntToStr(Secs)
    + '.' + IntToStr(mSecs) + 's;' + #13#10;
  result:=self;
end;

function TCSS.AnimationInfinite:TCSS;
begin
  Text:=Text + '-webkit-animation-iteration-count: infinite;' + #13#10;
  result:=Self;
end;

class function  TSuperStyle.AnimGlow(aFrom,aTo:TColor):String;
var
  mStyles:  TDarthStyleSheet;
  mWriter:  TCSS;
begin
  mStyles:=getStyleSheet;
  if mStyles<>NIL then
  Begin
    result:=w3_GetUniqueObjId;
    mWriter:=TCSS.Create;
    try
      mWriter
        .KeyFrames.inc(result + ' ')
        .Enter
          .from
          .enter.BoxShadow(0,0,0,0).leave.CRLF

          .PercentOf(50)
          .Enter.boxShadow(0,0,12,0,aTo).leave.CRLF

          .to
          .enter.BoxShadow(0,0,0,0).leave.CRLF
        .Leave .CRLF;

        writeln(mWriter.text);
        mStyles.Add(mWriter.Text);
        mWriter.text:='';

        mWriter
          .inc('.' + result + '_player ')
          .enter
            .AnimationName(result)
            .AnimationDuration(2,0)
            .AnimationInfinite
          .leave;
        mStyles.add(mWriter.text);
    finally
      mWriter.free;
    end;
    result:=result + '_player';
  end;
end;

class function TSuperStyle.EdgeRound(topleftP,toprightP,bottomleftP,
      bottomrightP:Integer):String;
var
  mStyles:  TDarthStyleSheet;
begin
  mStyles:=getStyleSheet;
  if mStyles<>NIL then
  Begin
    var mText:='border-radius:';
    if topleftP>0 then
    mText := mText + TInteger.ToPxStr(TInteger.ensureRange(topLeftP,0,100));

    if toprightP>0 then
    mText := mText + ' ' + TInteger.ToPxStr(TInteger.ensureRange(toprightP,0,100));

    if bottomRightP>0 then
    mText := mText + ' ' +TInteger.ToPxStr(TInteger.ensureRange(bottomRightP,0,100));

    if bottomLeftP>0 then
    mText := mText + ' ' +TInteger.ToPxStr(TInteger.ensureRange(bottomLeftP,0,100)) + ';';

    result:=mStyles.Add(result,mText);
  end;
end;

class function TSuperStyle.EdgeRound(Size:Integer):String;
var
  mStyles:  TDarthStyleSheet;
begin
  mStyles:=getStyleSheet;
  if mStyles<>NIL then
  result:=mStyles.Add(result,'border-radius: ' + TInteger.ToPxStr(Size));
end;

class function TSuperStyle.EdgeTopaz:String;
begin
  result:=TSuperStyle.EdgeRound(15,50,0,0);
end;

class function TSuperStyle.EdgeAngaro:String;
begin
  result:=TSuperStyle.EdgeRound(15,50,30,0);
end;

//############################################################################
// TDarthStyleSheet
//############################################################################

Constructor TDarthStyleSheet.Create;
var
  mDocument: THandle;
begin
  inherited Create;
  mDocument:=BrowserAPI.document;
  FHandle:=mDocument.createElement('style');
  FHandle.type := 'text/css';
  mDocument.getElementsByTagName('head')[0].appendChild(FHandle);
end;

Destructor TDarthStyleSheet.Destroy;
Begin
  if (FHandle) then
  FHandle.parentNode.removeChild(FHandle);
  FHandle:=null;
  Inherited;
end;

function TDarthStyleSheet.getCount:Integer;
Begin
  if (FHandle) then
  result:=getRules.length else
  result:=0;
end;

function TDarthStyleSheet.getItem(index:Integer):String;
Begin
  if (FHandle) then
  result:=getRules[index].cssText
end;

(* Takes height for differences between webkit, moz and IE *)
function TDarthStyleSheet.getRules:THandle;
var
  xRef: THandle;
Begin
  if (FHandle) then
  begin
    xRef:=getSheet;
    asm
      @result = (@xRef).cssRules || (@xRef).rules;
    end;
  end;
end;

(* Takes height for differences between webkit, moz and IE *)
function TDarthStyleSheet.getSheet:THandle;
var
  xRef: THandle;
Begin
  if (FHandle) then
  begin
    xRef:=FHandle;
    asm
      @result = (@xRef).styleSheet || (@xRef).sheet;
    end;
  end;
end;

class procedure TDarthStyleSheet.addClassToElement(const aElement:THandle;
      const aName:String);
Begin
 w3_AddClass( aElement,aName);
end;

class procedure TDarthStyleSheet.removeClassFromElement(const aElement:THandle;
      const aName:String);
Begin
  w3_RemoveClass(aElement,aName);
end;

class function  TDarthStyleSheet.findClassInElement(const aElement:THandle;
      const aName:String):Boolean;
Begin
  result:=w3_hasClass(aElement,aName);
end;

Procedure TDarthStyleSheet.Add(const aRuleText:String);
var
  mDocument: THandle;
  mSheet: THandle;
Begin
  mDocument:=BrowserAPI.document;
  if (FHandle) then
  Begin
    mSheet:=getSheet;
    if not (mSheet.insertRule) then
    mSheet.addRule(aRuleText) else
    mSheet.insertRule(aRuleText,0);
  end;
end;

function TDarthStyleSheet.Add(aName:String;const aRules:String):String;
var
  mDocument: THandle;
  mSheet: THandle;
Begin
  aName:=trim(aName);
  if length(aName)=0 then
  aName:=w3_GetUniqueObjId;

  mDocument:=BrowserAPI.document;
  if (FHandle) then
  Begin
    mSheet:=getSheet;
    if not (mSheet.insertRule) then
    mSheet.addRule('.' + aName,aRules) else
    mSheet.insertRule('.' + aName + '{' + aRules + '}',0);
  end;

  result:=aName;
end;

end.

Now inside the AnimGlow function in TSuperClass, you will notice that we have pretty much gone overboard with linear-gradients. Just ignore this for now, we will be dealing with each section one by one.

HTML5 valentines card

HTML5 valentines card, pure animated CSS

[coded by me back in 2012 to my GF]

But first, let’s get to know the commands of TCC. We start with the simple 1:1 type commands and leave the more advanced for later.

TCSS functions

  • Enter = {
  • Leave = }
  • To = to
  • From = from
  • PercentOf(Value) = Value%
  • IntOf(Value) = Value
  • ColorOf(Color) = $hex-color-value
  • inc(string) = add plain text to buffer
  • CRLF = carriage return and linefeed (#13#10, or just #13)
  • OpenParam = (
  • CloseParam = )
  • ColorPercent(Value,Color) = $hex-color-value %value,

The above functions map to simple CSS constructs. For instance, to generate a small JSON object you would write:

 .enter .inc(&amp;quot;value = 12;&amp;quot;) .leave 

And if you check the “text” property you find this:

  {
    value = 12;
  }

These types of functions are not so much time helpers as they are space helpers. It’s the other, high level functions which greatly simplify css generation.

For instance, a complex multi-colored, percentage-divided angular linear-gradient (phew!) can now be reduced to this:

.beginComplexGradient(80)
  .ColorPercent(10,clRed)
  .colorPercent(40,clBlue)
  .colorPercent(50,clWhite)
.endComplexGradient

This results in a variation of this (more compact):

background: -webkit-linear-gradient
   (
     80deg,
     #ff0000 10%,
     #0000ff 40%,
     #ffffff 50%
   );

When you start to build ever more complex shapes (this class is not yet done, not by a longshot) the generated CSS will become more and more complex, meaning that you save more and more time and headache; yet your code remain easy to read and maintain. At least once you know the basics of the class and what is going on.

Odd syntax

If you think the syntax is a bit odd then yes, we are using a fairly modern tecnique – where all functions return a reference to its “self”. This means you can call function after function in the same object without line-break.

At the same time we add data to an internal property (in this case cleverly called “text”). When all is done and ready we collect the output from Text and voila — we have our magic CSS style(s) and animations.

I will keep you all updated on this development. It will not be included in the next update of Smart Mobile Studio, because — well, it’s just a little snippet I’ve been playing with for half an hour. But it may make the next update after that, when it’s more mature and advanced.

Until then, play around with it and add more CSS wrappers (!)

Swirly graphics

April 9, 2015 Leave a comment

Just did a small 10 minute experiment in Smart Pascal. Essentially what I want is to be able to pre-calculate a nice soft path between X number of points. Which is essentially what is known as a bezier curve. This can be used in many situations from plotting pixels, moving sprites or indeed – animating elements with CSS.

Here is my 10 minute experiment:

function bezier(p1,p2,p3,p4:TPoint;t:float):TPoint;
begin
  var mum1 := 1- t;
  var mum13 := mum1 * mum1 * mum1;
  var mu3 := t * t * t;

  result:=TPoint.Create(
    round(mum13 * p1.x + 3*t * mum1*mum1 * p2.x+3*t*t * mum1*p3.x + mu3*p4.x),
    round(mum13 * p1.y + 3*t * mum1*mum1 * p2.y+3*t*t * mum1*p3.y + mu3*p4.y) );
end;

procedure TForm1.W3Button1Click(Sender: TObject);
var
  mBitmap:  TBitmap;
  x:  Integer;
  p1,p2,p3,p4:  TPoint;
begin
  mBitmap:=TBitmap.create;
  try
    mBitmap.Allocate(400,400);

    p1:=TPoint.Create(10,60);
    p2:=Tpoint.create(100,40);
    p3:=Tpoint.create(180,60);
    p4:=Tpoint.create(200,180);

    mBitmap.canvas.pen.color:=clRed;
    mBitmap.canvas.MoveTo(p1);
    mBitmap.canvas.lineTo(p2);
    mBitmap.canvas.lineto(p3);
    mBitmap.Canvas.lineto(p4);

    var f :=0.0;
    for x:=0 to 100 do
    begin
      f:=x / 100;
      var mpt := bezier(p1,p2,p3,p4,f);
      writeln(mpt.toString());
      mBitmap.canvas.Pixels[mpt.x,mpt.y]:=clBlue;
    end;

    w3panel1.Background.FromURL(mBitmap.Canvas.ToDataURL('png'));

  finally
    mBitmap.free;
  end;
end;

Lo and behold the mighty swirley path!

Oh its so nice and soft and swirley

Oh its so nice and soft and swirley