summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--AUTHORS1
-rw-r--r--COPYING674
-rw-r--r--ChangeLog0
-rw-r--r--Makefile.am2
-rw-r--r--NEWS12
-rw-r--r--README18
-rw-r--r--configure.ac37
-rw-r--r--doc/wwwlite.texi107
-rw-r--r--src/Makefile.am10
-rw-r--r--src/blockbox.c50
-rw-r--r--src/blockbox.h50
-rw-r--r--src/browserbox.c1433
-rw-r--r--src/browserbox.h133
-rw-r--r--src/documentbox.c489
-rw-r--r--src/documentbox.h70
-rw-r--r--src/inlinebox.c585
-rw-r--r--src/inlinebox.h119
-rw-r--r--src/main.c134
-rw-r--r--src/tablebox.c407
-rw-r--r--src/tablebox.h84
20 files changed, 4415 insertions, 0 deletions
diff --git a/AUTHORS b/AUTHORS
new file mode 100644
index 0000000..0e17051
--- /dev/null
+++ b/AUTHORS
@@ -0,0 +1 @@
+defanor <defanor@uberspace.net>
diff --git a/COPYING b/COPYING
new file mode 100644
index 0000000..f288702
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,674 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ <program> Copyright (C) <year> <name of author>
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<https://www.gnu.org/licenses/>.
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+<https://www.gnu.org/licenses/why-not-lgpl.html>.
diff --git a/ChangeLog b/ChangeLog
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/ChangeLog
diff --git a/Makefile.am b/Makefile.am
new file mode 100644
index 0000000..12a52e6
--- /dev/null
+++ b/Makefile.am
@@ -0,0 +1,2 @@
+SUBDIRS = src doc
+dist_doc_DATA = README
diff --git a/NEWS b/NEWS
new file mode 100644
index 0000000..97452e1
--- /dev/null
+++ b/NEWS
@@ -0,0 +1,12 @@
+ -*- outline -*-
+
+* 2019-08-17, a more complete prototype
+ At about 3 KLOC, now there is (rudimentary) support for tables with
+ colspans and rowspans, form submission, tabs, history navigation.
+
+* 2019-07-18, a prototype
+ At about 2000 lines it includes word wrapping and caching, clickable
+ and focusable links, text selection, inline GTK widgets mixed with
+ texts, streaming parsing and UI building, asynchronous image
+ loading. Handled HTML elements include h1-h6, p, a, br, img, pre,
+ ul, li, dl, dt, dd, input, sub, sup, code, i, em, b, strong.
diff --git a/README b/README
new file mode 100644
index 0000000..7c35f7b
--- /dev/null
+++ b/README
@@ -0,0 +1,18 @@
+WWWLite, a lightweight web browser.
+
+Hypertext documents are targeted, without per-document styling or
+executable scripts.
+
+Widely available and well-maintained tools are used, aiming a good
+system integration and easy building. System-wide colour and font
+settings are respected, parsing and UI building are incremental, image
+loading is asynchronous, word cache is used.
+
+At this stage it is rather a proof of concept: many features are
+implemented just to the point where it is apparent that they will not
+require major design changes to complete; there is not much of error
+handling or optimisation, no configuration. Though it is already
+capable of rendering and navigating HTML documents.
+
+A screenshot is available at
+<https://defanor.uberspace.net/pictures/wwwlite.png>.
diff --git a/configure.ac b/configure.ac
new file mode 100644
index 0000000..474e082
--- /dev/null
+++ b/configure.ac
@@ -0,0 +1,37 @@
+# -*- Autoconf -*-
+# Process this file with autoconf to produce a configure script.
+
+AC_PREREQ([2.69])
+AC_INIT([WWWLite], [0.0.0], [defanor@uberspace.net])
+AM_INIT_AUTOMAKE([-Wall])
+AC_CONFIG_SRCDIR([src/main.c])
+AC_CONFIG_FILES([Makefile src/Makefile doc/Makefile])
+AC_CONFIG_HEADERS([config.h])
+
+# Checks for programs.
+AC_PROG_CC
+AM_PROG_CC_C_O
+
+# Checks for libraries.
+PKG_CHECK_MODULES([LIBSOUP], [libsoup-2.4])
+AC_SUBST(LIBSOUP_CFLAGS)
+AC_SUBST(LIBSOUP_LIBS)
+
+PKG_CHECK_MODULES([LIBXML], [libxml-2.0])
+AC_SUBST(LIBXML_CFLAGS)
+AC_SUBST(LIBXML_LIBS)
+
+PKG_CHECK_MODULES([GTK3], [gtk+-3.0])
+AC_SUBST(GTK3_CFLAGS)
+AC_SUBST(GTK3_LIBS)
+
+# Checks for header files.
+
+# Checks for typedefs, structures, and compiler characteristics.
+
+# Checks for library functions.
+AC_FUNC_MALLOC
+AC_FUNC_REALLOC
+AC_CHECK_FUNCS([strdup])
+
+AC_OUTPUT
diff --git a/doc/wwwlite.texi b/doc/wwwlite.texi
new file mode 100644
index 0000000..200c254
--- /dev/null
+++ b/doc/wwwlite.texi
@@ -0,0 +1,107 @@
+\input texinfo @c -*-texinfo-*-
+
+@setfilename wwwlite.info
+@settitle WWWLite
+
+@contents
+
+@node Top
+@top WWWLite
+
+A lightweight web browser.
+
+@menu
+* Implementation:: Development documentation
+@end menu
+
+@node Implementation
+@chapter Implementation
+
+This chapter describes the reasoning behind design and technology
+choices, and the overall architecture.
+
+GTK+ is used because it is a widely available and well-maintained GUI
+toolkit, and Pango is its related project. Pango is used because complex
+text rendering respecting system-wide settings is desired, and Pango
+does just that, while also being easily available and well-maintained.
+Libsoup and libxml2 are chosen for similar reasons. In case if it will
+be desired to migrate from any of those libraries, or to support
+alternative ones, they would still provide a useful scaffolding for the
+time being.
+
+GTK's standard widgets do not include everything needed for an HTML
+document viewer. Most notably, HTML ``phrasing content'' allows texts to
+be mixed with hyperlinks, images, and various input elements, and it is
+expected that text selection would work across such elements. To achieve
+that, the @code{InlineBox} widget is implemented as a basic building
+block: it is a container widget that can contain texts and other
+elements, implementing word wrapping, link focus, selection rendering.
+Another important custom widget is @code{DocumentBox}, which manages
+selection (and similar tasks, such as handling of link clicks) across
+all the @code{InlineBox} widgets inside of it. A wrapper around that one
+is @code{BrowserBox}, which combines it with an address bar and a status
+bar, manages document loading and UI building. The latter is placed
+directly into main window's tabs.
+
+More on individual components is in the following sections.
+
+@section InlineBox
+
+An @code{InlineBox} can have children of 3 types: texts (the
+@code{IBText} object), line breaks (@code{IBBreak}), and regular GTK
+widgets (e.g., images, input fields). While texts and line breaks could
+be widgets too, tens or hundreds of thousands of widgets cause a GTK
+application to lag. Regular C structures could be used instead, but
+@code{GObject} objects are used because they already provide a way to
+distinguish between structure types, with no need for an additional
+wrapper.
+
+Line breaks are implemented as @code{InlineBox} children, since they can
+be inside a link (or a markup element), which would be more cumbersome
+to manage if it was spread across multiple @code{InlineBox} elements,
+and @code{InlineBox} better matches ``phrasing content'' semantics this
+way.
+
+@code{IBText} structures contain an allocation and reference a Pango
+layout of a single word, facilitating word caching. Whitespaces are
+handled as regular words, since they can vary in size and other text
+properties.
+
+@section TableBox
+
+@code{TableBox} is used for HTML tables, and @code{TableCell} is a
+subtype of @code{BlockBox}, which adds @var{colspan} and @var{rowspan}
+properties.
+
+@code{TableBox}'s @code{rows} correspond to HTML @code{tr} elements,
+each row contains a list of @code{TableCell} elements, corresponding to
+HTML @code{td} or @code{th}.
+
+@c TODO: document the algorithm.
+
+@section BlockBox
+
+@code{BlockBox} is a subtype of @code{GtkBox}. It only sets orientation
+to vertical, and the ``height for width'' GTK request mode instead of
+applying the default @code{GtkContainer} heuristics to determine that.
+
+@section DocumentBox
+
+@code{DocumentBox} is responsible for scrolling, link clicks, text
+selection management.
+
+@section BrowserBox
+
+@code{BrowserBox} combines an address bar, a @code{DocumentBox}, and a
+status bar. It also carries @code{BuilderState} and @code{SoupSession}.
+It is intended to be used for browser tabs, while on its own it
+implements a non-tabbed browser.
+
+@c TODO: describe UI building
+
+@section Main window
+
+The main window contains tabs, and tab management events are handled in
+@code{main.c}.
+
+@bye
diff --git a/src/Makefile.am b/src/Makefile.am
new file mode 100644
index 0000000..74c18e7
--- /dev/null
+++ b/src/Makefile.am
@@ -0,0 +1,10 @@
+AM_CFLAGS = -Werror -Wall -Wextra -Wno-unused-parameter
+# todo: add -pedantic later, allowing the draft to be relatively messy
+# for now
+
+bin_PROGRAMS = wwwlite
+
+wwwlite_SOURCES = main.c inlinebox.c documentbox.c blockbox.c tablebox.c browserbox.c
+noinst_HEADERS = inlinebox.h documentbox.h blockbox.h tablebox.h browserbox.h
+wwwlite_CFLAGS = $(LIBSOUP_CFLAGS) $(LIBXML_CFLAGS) $(GTK3_CFLAGS) $(AM_CFLAGS)
+wwwlite_LDADD = $(LIBSOUP_LIBS) $(LIBXML_LIBS) $(GTK3_LIBS)
diff --git a/src/blockbox.c b/src/blockbox.c
new file mode 100644
index 0000000..1b3b462
--- /dev/null
+++ b/src/blockbox.c
@@ -0,0 +1,50 @@
+/* WWWLite, a lightweight web browser.
+ Copyright (C) 2019 defanor
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+*/
+
+#include <gtk/gtk.h>
+#include "blockbox.h"
+#include "inlinebox.h"
+
+G_DEFINE_TYPE (BlockBox, block_box, GTK_TYPE_BOX);
+
+static GtkSizeRequestMode block_box_get_request_mode (GtkWidget *widget)
+{
+ return GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH;
+}
+
+static void
+block_box_class_init (BlockBoxClass *klass)
+{
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass);
+ widget_class->get_request_mode = block_box_get_request_mode;
+ return;
+}
+
+static void
+block_box_init (BlockBox *bb)
+{
+ return;
+}
+
+GtkWidget *block_box_new (guint spacing)
+{
+ BlockBox *bb = BLOCK_BOX(g_object_new(block_box_get_type(),
+ "orientation", GTK_ORIENTATION_VERTICAL,
+ "spacing", spacing,
+ NULL));
+ return GTK_WIDGET(bb);
+}
diff --git a/src/blockbox.h b/src/blockbox.h
new file mode 100644
index 0000000..6d581ed
--- /dev/null
+++ b/src/blockbox.h
@@ -0,0 +1,50 @@
+/* WWWLite, a lightweight web browser.
+ Copyright (C) 2019 defanor
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+*/
+
+#ifndef BLOCK_BOX_H
+#define BLOCK_BOX_H
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define BLOCK_BOX_TYPE (block_box_get_type())
+#define BLOCK_BOX(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), BLOCK_BOX_TYPE, BlockBox))
+#define BLOCK_BOX_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), BLOCK_BOX_TYPE, BlockBoxClass))
+#define IS_BLOCK_BOX(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), BLOCK_BOX_TYPE))
+#define IS_BLOCK_BOX_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), BLOCK_BOX_TYPE))
+
+typedef struct _BlockBox BlockBox;
+typedef struct _BlockBoxClass BlockBoxClass;
+
+struct _BlockBox
+{
+ GtkBox parent_instance;
+};
+
+struct _BlockBoxClass
+{
+ GtkBoxClass parent_class;
+};
+
+GType block_box_get_type(void) G_GNUC_CONST;
+GtkWidget *block_box_new(guint spacing);
+
+
+G_END_DECLS
+
+#endif
diff --git a/src/browserbox.c b/src/browserbox.c
new file mode 100644
index 0000000..fe69cde
--- /dev/null
+++ b/src/browserbox.c
@@ -0,0 +1,1433 @@
+/* WWWLite, a lightweight web browser.
+ Copyright (C) 2019 defanor
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+*/
+
+/* The code in this file is particularly messy, and some of it should
+ be reorganised. */
+
+#include <glib.h>
+#include <gtk/gtk.h>
+#include "browserbox.h"
+#include "inlinebox.h"
+#include "blockbox.h"
+#include "tablebox.h"
+#include "documentbox.h"
+#include <libxml/HTMLparser.h>
+#include <libsoup/soup.h>
+
+
+typedef struct _ImageSetData ImageSetData;
+struct _ImageSetData
+{
+ GtkImage *image;
+ BuilderState *bs;
+};
+
+
+
+G_DEFINE_TYPE (BuilderState, builder_state, G_TYPE_OBJECT);
+G_DEFINE_TYPE (BrowserBox, browser_box, BLOCK_BOX_TYPE);
+
+/* todo: move some of the properties into BrowserBox (or DocumentBox),
+ particularly the ones that are used after rendering. */
+static void builder_state_init (BuilderState *bs)
+{
+ bs->active = TRUE;
+ bs->vbox = NULL;
+ bs->docbox = NULL;
+ bs->root = NULL;
+ bs->stack = g_slist_alloc();
+ bs->stack->data = NULL;
+ bs->text_position = 0;
+ bs->current_attrs = pango_attr_list_new();
+ bs->current_link = NULL;
+ bs->current_word = NULL;
+ bs->ignore_text = FALSE;
+ bs->prev_space = TRUE;
+ bs->pre = FALSE;
+ bs->parser = NULL;
+ bs->uri = NULL;
+ bs->queued_identifiers = NULL;
+ bs->identifiers =
+ g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
+ bs->anchor_handler_id = 0;
+ bs->option_value = NULL;
+ bs->ol_numbers = NULL;
+ bs->current_form = NULL;
+}
+
+BuilderState *builder_state_new (GtkWidget *root)
+{
+ BuilderState *bs = g_object_new (BUILDER_STATE_TYPE, NULL);
+ bs->root = root;
+ GtkStyleContext *styleCtx = gtk_widget_get_style_context(root);
+ gtk_style_context_get_color(styleCtx, GTK_STATE_FLAG_LINK, &bs->link_color);
+ return bs;
+}
+
+void builder_state_dispose (GObject *self)
+{
+ BuilderState *bs = BUILDER_STATE(self);
+ if (bs->parser) {
+ htmlFreeParserCtxt(bs->parser);
+ bs->parser = NULL;
+ }
+ if (bs->stack) {
+ g_slist_free(bs->stack);
+ bs->stack = NULL;
+ }
+ if (bs->current_attrs) {
+ pango_attr_list_unref(bs->current_attrs);
+ bs->current_attrs = NULL;
+ }
+ if (bs->identifiers) {
+ g_hash_table_unref(bs->identifiers);
+ bs->identifiers = NULL;
+ }
+ if (bs->queued_identifiers) {
+ g_slist_free(bs->queued_identifiers);
+ bs->queued_identifiers = NULL;
+ }
+ if (bs->option_value) {
+ free(bs->option_value);
+ bs->option_value = NULL;
+ }
+ if (bs->ol_numbers) {
+ g_slist_free_full(bs->ol_numbers, g_free);
+ bs->ol_numbers = NULL;
+ }
+ G_OBJECT_CLASS (builder_state_parent_class)->dispose (self);
+}
+
+static void builder_state_class_init (BuilderStateClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ object_class->dispose = builder_state_dispose;
+}
+
+
+void scroll_to_identifier(BuilderState *bs, const char *identifier)
+{
+ GtkWidget *target = g_hash_table_lookup(bs->identifiers, identifier);
+ if (target) {
+ GtkAllocation widget_alloc, *alloc;
+ if (GTK_IS_WIDGET(target)) {
+ gtk_widget_get_allocation(target, &widget_alloc);
+ alloc = &widget_alloc;
+ } else if (IS_IB_TEXT(target)) {
+ alloc = &IB_TEXT(target)->alloc;
+ } else {
+ puts("Shouldn't happen");
+ return;
+ }
+ GtkAdjustment *adj =
+ gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(bs->docbox));
+ gtk_adjustment_set_value(adj, alloc->y);
+ gtk_scrolled_window_set_vadjustment(GTK_SCROLLED_WINDOW(bs->docbox),
+ adj);
+ }
+}
+
+void image_set (SoupSession *session, SoupMessage *msg, ImageSetData *isd)
+{
+ /* Just setting a whole image at once for now, progressive loading
+ is left for later. */
+ if (isd->bs->active && msg->response_body->data != NULL) {
+ GdkPixbufLoader *il = gdk_pixbuf_loader_new();
+ GError *err = NULL;
+ gdk_pixbuf_loader_write(il, (unsigned char *)msg->response_body->data,
+ msg->response_body->length, &err);
+ gdk_pixbuf_loader_close(il, &err);
+ GdkPixbuf *pb = gdk_pixbuf_loader_get_pixbuf(il);
+ if (pb != NULL) {
+ /* Temporarily scaling large images on loading: it's imprecise
+ and generally awkward, but better than embedding huge images.
+ Better to resize on window resize and along size allocation
+ in the future, but GTK is unhappy if it's done during size
+ allocation, and without storing the original image, it also
+ leads to poor quality (i.e., perhaps will need a custom
+ GtkImage subtype). */
+ int doc_width = gtk_widget_get_allocated_width(GTK_WIDGET(isd->bs->root));
+ int pb_width = gdk_pixbuf_get_width(pb);
+ int pb_height = gdk_pixbuf_get_height(pb);
+ if (pb_width > doc_width) {
+ GdkPixbuf *old_pb = pb;
+ int new_height = (double)pb_height * (double)doc_width / (double)pb_width;
+ if (new_height < 1) {
+ new_height = 1;
+ }
+ pb = gdk_pixbuf_scale_simple(old_pb, doc_width, new_height,
+ GDK_INTERP_BILINEAR);
+ }
+ if (pb != NULL) {
+ gtk_image_set_from_pixbuf(isd->image, pb);
+ if (pb_width > doc_width) {
+ g_object_unref(pb);
+ }
+ }
+ }
+ g_object_unref(il);
+ }
+ free(isd);
+ g_object_unref(isd->bs);
+}
+
+
+/* Word cache utilities */
+
+guint pango_attr_hash (PangoAttribute *attr)
+{
+ /* todo: that's not a great hash, maybe improve later */
+ return attr->klass->type ^ attr->start_index ^ attr->end_index;
+}
+
+guint wck_hash (WordCacheKey *wck)
+{
+ guint attr_hash = 0;
+ PangoAttrIterator *pai = pango_attr_list_get_iterator(wck->attrs);
+ GSList *attrs = pango_attr_iterator_get_attrs(pai);
+ GSList *ai;
+ for (ai = attrs; ai; ai = ai->next) {
+ attr_hash ^= pango_attr_hash(ai->data);
+ }
+ g_slist_free_full(attrs, (GDestroyNotify)pango_attribute_destroy);
+ pango_attr_iterator_destroy(pai);
+ guint text_hash = g_str_hash(wck->text);
+ return attr_hash ^ text_hash;
+}
+
+gboolean wck_equal (WordCacheKey *wck1, WordCacheKey *wck2)
+{
+ PangoAttrIterator *pai1 = pango_attr_list_get_iterator(wck1->attrs);
+ PangoAttrIterator *pai2 = pango_attr_list_get_iterator(wck2->attrs);
+ GSList *attrs1 = pango_attr_iterator_get_attrs(pai1);
+ GSList *attrs2 = pango_attr_iterator_get_attrs(pai2);
+ GSList *ai1, *ai2;
+ for (ai1 = attrs1, ai2 = attrs2; ai1 || ai2; ai1 = ai1->next, ai2 = ai2->next) {
+ if (( ! (ai1 && ai2)) || ( ! pango_attribute_equal(ai1->data, ai2->data))) {
+ g_slist_free_full(attrs1, (GDestroyNotify)pango_attribute_destroy);
+ g_slist_free_full(attrs2, (GDestroyNotify)pango_attribute_destroy);
+ pango_attr_iterator_destroy(pai1);
+ pango_attr_iterator_destroy(pai2);
+ return FALSE;
+ }
+ }
+ g_slist_free_full(attrs1, (GDestroyNotify)pango_attribute_destroy);
+ g_slist_free_full(attrs2, (GDestroyNotify)pango_attribute_destroy);
+ pango_attr_iterator_destroy(pai1);
+ pango_attr_iterator_destroy(pai2);
+ return g_str_equal(wck1->text, wck2->text);
+}
+
+
+
+
+PangoLayout *get_layout(GtkWidget *widget, const gchar *text,
+ PangoAttrList *attrs)
+{
+ WordCacheKey *wck = malloc(sizeof(WordCacheKey));
+ wck->text = strdup(text);
+ wck->attrs = attrs;
+ pango_attr_list_ref(wck->attrs);
+ PangoLayout *pl = g_hash_table_lookup(word_cache, wck);
+ if (pl == NULL) {
+ pl = gtk_widget_create_pango_layout(widget, text);
+ pango_layout_set_attributes(pl, attrs);
+ g_hash_table_insert(word_cache, wck, pl);
+ } else {
+ free(wck->text);
+ pango_attr_list_unref(wck->attrs);
+ free(wck);
+ }
+ return pl;
+}
+
+PangoAttrList *shift_attributes(PangoAttrList *src_attrs, guint len)
+{
+ PangoAttrIterator *pai;
+ PangoAttrList *new_attrs;
+ PangoAttribute *attr;
+ GSList *iter_al, *al;
+ new_attrs = pango_attr_list_new();
+ pai = pango_attr_list_get_iterator(src_attrs);
+ if (pai != NULL) {
+ do {
+ iter_al = pango_attr_iterator_get_attrs(pai);
+ for (al = iter_al; al; al = al->next) {
+ attr = al->data;
+ if (attr->end_index > len || attr->end_index == G_MAXUINT) {
+ attr->start_index = 0;
+ if (attr->end_index != G_MAXUINT) {
+ attr->end_index -= len;
+ }
+ pango_attr_list_insert(new_attrs, attr);
+ } else {
+ pango_attribute_destroy(attr);
+ }
+ }
+ g_slist_free(iter_al);
+ } while (pango_attr_iterator_next(pai));
+ pango_attr_iterator_destroy(pai);
+ }
+ pango_attr_list_unref(src_attrs);
+ return new_attrs;
+}
+
+void attribute_start(PangoAttrList *attrs, PangoAttribute *attr, guint position)
+{
+ attr->start_index = position;
+ pango_attr_list_insert(attrs, attr);
+}
+
+/* todo: better tracking of attributes is needed; this would end all
+ the matching attributes instead of just a particular one */
+PangoAttrList *attribute_end(PangoAttrList *attrs,
+ PangoAttrType type, guint position)
+{
+ PangoAttrIterator *pai;
+ PangoAttrList *new_attrs;
+ PangoAttribute *attr;
+ GSList *iter_al, *al;
+ new_attrs = pango_attr_list_new();
+ pai = pango_attr_list_get_iterator(attrs);
+ if (pai != NULL) {
+ do {
+ iter_al = pango_attr_iterator_get_attrs(pai);
+ for (al = iter_al; al; al = al->next) {
+ attr = al->data;
+ if (attr->klass->type == type && attr->end_index > position) {
+ attr->end_index = position;
+ }
+ pango_attr_list_change(new_attrs, attr);
+ }
+ g_slist_free(iter_al);
+ } while (pango_attr_iterator_next(pai));
+ pango_attr_iterator_destroy(pai);
+ }
+ pango_attr_list_unref(attrs);
+ return new_attrs;
+}
+
+void ensure_inline_box (BuilderState *bs)
+{
+ if (! IS_INLINE_BOX(bs->stack->data)) {
+ if (GTK_IS_CONTAINER(bs->stack->data)) {
+ InlineBox *ib = inline_box_new();
+ bs->text_position = 0;
+ gtk_container_add (GTK_CONTAINER (bs->stack->data), GTK_WIDGET (ib));
+ gtk_widget_show_all (GTK_WIDGET(ib));
+ bs->stack = g_slist_prepend(bs->stack, ib);
+ } else {
+ puts("neither a text nor a container");
+ return;
+ }
+ }
+}
+
+void anchor_allocated (GtkWidget *widget,
+ GdkRectangle *alloc,
+ BuilderState *bs)
+{
+ GtkAdjustment *adj =
+ gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(bs->docbox));
+ gtk_adjustment_set_value(adj, alloc->y);
+ gtk_scrolled_window_set_vadjustment(GTK_SCROLLED_WINDOW(bs->docbox),
+ adj);
+ g_signal_handler_disconnect(widget, bs->anchor_handler_id);
+ bs->anchor_handler_id = 0;
+}
+
+IBText *add_word(BuilderState *bs, gchar *word, PangoAttrList **attrs)
+{
+ ensure_inline_box(bs);
+ InlineBox *ib = bs->stack->data;
+ IBText *ibt = NULL;
+ if (word[0] != 0) {
+ PangoLayout *pl = get_layout(GTK_WIDGET(ib), word, *attrs);
+ ibt = ib_text_new(pl);
+ inline_box_add_text(ib, ibt);
+ *attrs = shift_attributes(*attrs, strlen(word));
+ if (bs->queued_identifiers) {
+ GSList *ii;
+ for (ii = bs->queued_identifiers; ii; ii = ii->next) {
+ const char *fragment = soup_uri_get_fragment(bs->uri);
+ if (fragment && bs->anchor_handler_id == 0 &&
+ strcmp(ii->data, fragment) == 0) {
+ bs->anchor_handler_id =
+ g_signal_connect (ib, "size-allocate",
+ G_CALLBACK(anchor_allocated), bs);
+ }
+ g_hash_table_insert(bs->identifiers, ii->data, ibt);
+ }
+ g_slist_free(bs->queued_identifiers);
+ bs->queued_identifiers = NULL;
+ }
+ }
+ return ibt;
+}
+
+
+
+
+void history_add (BrowserBox *bb, SoupURI *uri)
+{
+ if (bb->history_position && soup_uri_equal(uri, bb->history_position->data)) {
+ return;
+ }
+ if (bb->history_position != NULL && bb->history_position->next != NULL) {
+ GList *tail = bb->history_position->next;
+ bb->history_position->next = NULL;
+ tail->prev = NULL;
+ g_list_free(tail);
+ }
+ bb->history = g_list_append(bb->history, soup_uri_copy(uri));
+ bb->history_position = g_list_last(bb->history);
+}
+
+gboolean history_back (BrowserBox *bb)
+{
+ if (bb->history_position != NULL && bb->history_position->prev) {
+ bb->history_position = bb->history_position->prev;
+ document_request(bb, soup_uri_copy(bb->history_position->data));
+ return TRUE;
+ }
+ return FALSE;
+}
+
+gboolean history_forward (BrowserBox *bb)
+{
+ if (bb->history_position != NULL && bb->history_position->next) {
+ bb->history_position = bb->history_position->next;
+ document_request(bb, soup_uri_copy(bb->history_position->data));
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static void form_submit (GtkButton *button, gpointer ptr)
+{
+ Form *form = ptr;
+ BrowserBox *bb = BROWSER_BOX(form->submission_data);
+ gchar *method = "GET";
+ puts("submitting");
+ if (form->method != NULL) {
+ if (g_ascii_strncasecmp(form->method, "post", 4) == 0) {
+ method = "POST";
+ }
+ }
+ gchar *uri_str = soup_uri_to_string(form->action, FALSE);
+
+ if (form->enctype == ENCTYPE_URLENCODED) {
+ GHashTable *fields = g_hash_table_new(g_str_hash, g_str_equal);
+ GList *fi;
+ for (fi = form->fields; fi; fi = fi->next) {
+ FormField *ff = fi->data;
+ if (GTK_IS_ENTRY(ff->widget)) {
+ g_hash_table_insert(fields, ff->name,
+ (gpointer)gtk_entry_get_text(GTK_ENTRY(ff->widget)));
+ } else if (GTK_IS_COMBO_BOX(ff->widget)) {
+ if (gtk_combo_box_get_active_id(GTK_COMBO_BOX(ff->widget)) != NULL) {
+ g_hash_table_insert(fields, ff->name,
+ (gpointer)gtk_combo_box_get_active_id(GTK_COMBO_BOX(ff->widget)));
+ }
+ }
+ }
+ SoupMessage *sm = soup_form_request_new_from_hash(method, uri_str, fields);
+ g_hash_table_unref(fields);
+ history_add(bb, soup_message_get_uri(sm));
+ document_request_sm(bb, sm);
+ } else if (form->enctype == ENCTYPE_MULTIPART) {
+ puts("multipart, not supported yet");
+ } else if (form->enctype == ENCTYPE_PLAIN) {
+ puts("plain, not supported yet");
+ }
+ g_free(uri_str);
+}
+
+
+
+void sax_characters (BrowserBox *bb, const xmlChar * ch, int len)
+{
+ BuilderState *bs = bb->builder_state;
+ if (bs->ignore_text || IS_TABLE_BOX(bs->stack->data)) {
+ return;
+ }
+
+ char *value = malloc(len + 1);
+ g_strlcpy(value, (const char*)ch, len + 1);
+
+ if (GTK_IS_COMBO_BOX_TEXT(bs->stack->data)) {
+ if (bs->option_value) {
+ gtk_combo_box_text_append(GTK_COMBO_BOX_TEXT(bs->stack->data),
+ bs->option_value, value);
+ free(bs->option_value);
+ bs->option_value = NULL;
+ }
+ free(value);
+ return;
+ }
+
+ ensure_inline_box(bs);
+
+ gint i = 0, j = 0;
+ while (i < len) {
+ if (value[i] == ' ' || value[i] == '\n' ||
+ value[i] == '\r' || value[i] == '\t') {
+ gchar c = value[i];
+ value[i] = 0;
+ if (bs->current_word != NULL) {
+ bs->current_word =
+ realloc(bs->current_word,
+ strlen(bs->current_word) + strlen(value + j) + 1);
+ g_strlcpy(bs->current_word + strlen(bs->current_word),
+ value + j, strlen(value + j) + 1);
+ add_word(bs, bs->current_word, &bs->current_attrs);
+ free(bs->current_word);
+ bs->current_word = NULL;
+ } else {
+ add_word(bs, value + j, &bs->current_attrs);
+ }
+ bs->text_position += strlen(value + j);
+ if (bs->pre && c == '\n') {
+ inline_box_break(INLINE_BOX(bs->stack->data));
+ } else {
+ if (bs->pre || ! bs->prev_space) {
+ add_word(bs, " ", &bs->current_attrs);
+ bs->text_position += 1;
+ bs->prev_space = TRUE;
+ }
+ }
+ j = i + 1;
+ } else {
+ bs->prev_space = FALSE;
+ }
+ i++;
+ }
+ if (i > j) {
+ if (bs->current_word == NULL) {
+ bs->current_word = strdup(value + j);
+ } else {
+ bs->current_word =
+ realloc(bs->current_word,
+ strlen(bs->current_word) + strlen(value + j) + 1);
+ g_strlcpy(bs->current_word + strlen(bs->current_word),
+ value + j, strlen(value + j) + 1);
+ }
+ bs->text_position += strlen(value + j);
+ }
+ free(value);
+}
+
+gboolean element_is_blocking (const char *name)
+{
+ /* Not including <div> elements: the results of their inclusion
+ aren't always good, and according to the specification they have
+ no special meaning at all. */
+ return (strcmp(name, "p") == 0 ||
+ strcmp(name, "h1") == 0 || strcmp(name, "h2") == 0 ||
+ strcmp(name, "h3") == 0 || strcmp(name, "h4") == 0 ||
+ strcmp(name, "h5") == 0 || strcmp(name, "h6") == 0 ||
+ strcmp(name, "pre") == 0 || strcmp(name, "ul") == 0 ||
+ strcmp(name, "ol") == 0 || strcmp(name, "li") == 0 ||
+ strcmp(name, "dl") == 0 || strcmp(name, "dt") == 0 ||
+ strcmp(name, "dd") == 0 || strcmp(name, "table") == 0 ||
+ strcmp(name, "td") == 0 || strcmp(name, "th") == 0 ||
+ strcmp(name, "tr") == 0
+ );
+}
+
+gboolean element_flushes_text (const char *name)
+{
+ return (element_is_blocking (name) ||
+ (strcmp(name, "br") == 0 || strcmp(name, "img") == 0 ||
+ strcmp(name, "input") == 0 || strcmp(name, "select") == 0
+ ));
+}
+
+void sax_start_element (BrowserBox *bb,
+ const xmlChar * u_name,
+ const xmlChar ** attrs)
+{
+ BuilderState *bs = bb->builder_state;
+ const char *name = (const char*)u_name;
+
+ if (IS_INLINE_BOX(bs->stack->data)) {
+ if (element_flushes_text(name)) {
+ if (bs->current_word != NULL) {
+ add_word(bs, bs->current_word, &bs->current_attrs);
+ free(bs->current_word);
+ bs->current_word = NULL;
+ }
+ bs->prev_space = TRUE;
+ }
+ if (element_is_blocking(name)) {
+ GSList *next = bs->stack->next;
+ if (next != NULL) {
+ g_slist_free_1(bs->stack);
+ bs->stack = next;
+ }
+ }
+ /* Line breaks */
+ if (strcmp(name, "br") == 0) {
+ inline_box_break(INLINE_BOX(bs->stack->data));
+ }
+ }
+
+ if (IS_BLOCK_BOX(bs->stack->data)) {
+ /* Elements that (may) need inline boxes */
+ if (strcmp(name, "a") == 0 ||
+ strcmp(name, "br") == 0 ||
+ strcmp(name, "img") == 0 ||
+ strcmp(name, "select") == 0 ||
+ strcmp(name, "input") == 0) {
+ ensure_inline_box(bs);
+ }
+ }
+
+ if (IS_BLOCK_BOX(bs->stack->data)) {
+ /* Lists */
+ if (strcmp(name, "dl") == 0 || strcmp(name, "ul") == 0 ||
+ strcmp(name, "ol") == 0) {
+ /* todo: maybe use a dedicated widget for ul and ol */
+ GtkWidget *dl = block_box_new(0);
+ gtk_container_add (GTK_CONTAINER (bs->stack->data), GTK_WIDGET (dl));
+ gtk_widget_show_all(dl);
+ bs->stack = g_slist_prepend(bs->stack, dl);
+ if ((strcmp(name, "ol") == 0) || (strcmp(name, "ul") == 0)) {
+ guint *num = malloc(sizeof(guint));
+ if (strcmp(name, "ol") == 0) {
+ *num = 1;
+ } else if (strcmp(name, "ul") == 0) {
+ *num = 0;
+ }
+ bs->ol_numbers = g_slist_prepend(bs->ol_numbers, num);
+ }
+ }
+
+ if (strcmp(name, "dd") == 0) {
+ GtkWidget *dd = block_box_new(10);
+ gtk_container_add (GTK_CONTAINER (bs->stack->data), GTK_WIDGET (dd));
+ gtk_widget_show_all(dd);
+ bs->stack = g_slist_prepend(bs->stack, dd);
+ gtk_widget_set_margin_start(bs->stack->data, 32);
+ }
+
+ if (bs->ol_numbers) {
+ if (strcmp(name, "li") == 0) {
+ GtkWidget *hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 5);
+ gchar *str;
+ guint num = *((guint*)bs->ol_numbers->data);
+ if (num == 0) {
+ str = "*";
+ } else {
+ str = g_strdup_printf("%u.", num);
+ *((guint*)bs->ol_numbers->data) = num + 1;
+ }
+ GtkWidget *lbl = gtk_label_new(str);
+ if (num > 0) {
+ g_free(str);
+ }
+ GtkWidget *vbox = block_box_new(10);
+ gtk_widget_set_valign(lbl, GTK_ALIGN_START);
+ gtk_container_add (GTK_CONTAINER (hbox), GTK_WIDGET (lbl));
+ gtk_container_add (GTK_CONTAINER (hbox), GTK_WIDGET (vbox));
+ gtk_container_add (GTK_CONTAINER (bs->stack->data), hbox);
+ gtk_widget_show_all(hbox);
+ bs->stack = g_slist_prepend(bs->stack, hbox);
+ bs->stack = g_slist_prepend(bs->stack, vbox);
+ }
+ }
+
+ /* Tables */
+ if (strcmp(name, "table") == 0) {
+ GtkWidget *tb = table_box_new();
+ gtk_container_add (GTK_CONTAINER (bs->stack->data), tb);
+ gtk_widget_show_all(tb);
+ bs->stack = g_slist_prepend(bs->stack, tb);
+ }
+
+ /* Preformatted texts */
+ if (strcmp(name, "pre") == 0 && IS_BLOCK_BOX(bs->stack->data)) {
+ InlineBox *ib = inline_box_new();
+ bs->text_position = 0;
+ ib->wrap = FALSE;
+ gtk_container_add (GTK_CONTAINER (bs->stack->data), GTK_WIDGET (ib));
+ gtk_widget_show_all(GTK_WIDGET(ib));
+ bs->stack = g_slist_prepend(bs->stack, ib);
+ bs->pre = TRUE;
+ attribute_start(bs->current_attrs, pango_attr_family_new("mono"), 0);
+ }
+ }
+
+ if (IS_TABLE_BOX(bs->stack->data)) {
+ if (strcmp(name, "tr") == 0) {
+ table_box_add_row(bs->stack->data);
+ }
+
+ if (TABLE_BOX(bs->stack->data)->rows != NULL) {
+ if (strcmp(name, "td") == 0 || strcmp(name, "th") == 0) {
+ GtkWidget *tc = table_cell_new();
+ if (attrs != NULL) {
+ const gchar *rowspan = NULL, *colspan = NULL;
+ guint i;
+ for (i = 0; attrs[i]; i += 2){
+ if (g_strcmp0((const char*)attrs[i], "colspan") == 0) {
+ colspan = (const char*)attrs[i+1];
+ }
+ if (g_strcmp0((const char*)attrs[i], "rowspan") == 0) {
+ rowspan = (const char*)attrs[i+1];
+ }
+ }
+ if (rowspan != NULL) {
+ sscanf(rowspan, "%u", &(TABLE_CELL(tc)->rowspan));
+ if (TABLE_CELL(tc)->rowspan > 65534) {
+ TABLE_CELL(tc)->rowspan = 65534;
+ } else if (TABLE_CELL(tc)->rowspan == 0) {
+ TABLE_CELL(tc)->rowspan = 1;
+ }
+ }
+ if (colspan != NULL) {
+ sscanf(colspan, "%u", &(TABLE_CELL(tc)->colspan));
+ if (TABLE_CELL(tc)->colspan > 65534) {
+ TABLE_CELL(tc)->colspan = 65534;
+ } else if (TABLE_CELL(tc)->colspan == 0) {
+ TABLE_CELL(tc)->colspan = 1;
+ }
+ }
+ }
+ gtk_container_add (GTK_CONTAINER (bs->stack->data), tc);
+ gtk_widget_show_all(tc);
+ bs->stack = g_slist_prepend(bs->stack, tc);
+ }
+ }
+ }
+
+ /* Ignored */
+ if (strcmp(name, "head") == 0 || strcmp(name, "script") == 0 ||
+ strcmp(name, "style") == 0) {
+ bs->ignore_text = TRUE;
+ }
+
+ /* Images */
+ if (IS_INLINE_BOX(bs->stack->data)) {
+ if (strcmp(name, "img") == 0) {
+ guint i;
+ const char *src = NULL;
+ if (attrs != NULL) {
+ for (i = 0; attrs[i]; i += 2){
+ if (strcmp((const char*)attrs[i], "src") == 0) {
+ src = (const char*)attrs[i+1];
+ }
+ }
+ }
+ if (src != NULL) {
+ GtkWidget *image = gtk_image_new_from_file(NULL);
+ if (image != NULL) {
+ /* todo: progressive image loading */
+ gtk_container_add (GTK_CONTAINER (bs->stack->data), image);
+ gtk_widget_show_all(image);
+
+ SoupURI *uri = soup_uri_new_with_base(bs->uri, src);
+ SoupMessage *sm = soup_message_new_from_uri("GET", uri);
+ soup_uri_free(uri);
+ ImageSetData *isd = malloc(sizeof(ImageSetData));
+ isd->image = GTK_IMAGE(image);
+ isd->bs = bs;
+ g_object_ref(bs);
+ soup_session_queue_message(bb->soup_session, sm,
+ (SoupSessionCallback)image_set, isd);
+ if (bs->current_link != NULL) {
+ bs->current_link->objects =
+ g_list_prepend(bs->current_link->objects, image);
+ }
+ }
+ }
+ }
+
+ /* Inputs */
+ if (strcmp(name, "input") == 0) {
+ guint i;
+ const char *type = NULL, *value = NULL, *a_name = NULL;
+ if (attrs != NULL) {
+ for (i = 0; attrs[i]; i += 2){
+ if (g_strcmp0((const char*)attrs[i], "type") == 0) {
+ type = (const char*)attrs[i+1];
+ }
+ if (g_strcmp0((const char*)attrs[i], "value") == 0) {
+ value = (const char*)attrs[i+1];
+ }
+ if (g_strcmp0((const char*)attrs[i], "name") == 0) {
+ a_name = (const char*)attrs[i+1];
+ }
+ }
+ }
+ GtkWidget *input = NULL;
+ if (g_strcmp0(type, "submit") == 0) {
+ input =
+ gtk_button_new_with_label(value == NULL ? "submit" : value);
+ if (bs->current_form != NULL) {
+ g_signal_connect (input, "clicked",
+ G_CALLBACK(form_submit), bs->current_form);
+ }
+ } else if (g_strcmp0(type, "checkbox") == 0) {
+ input = gtk_check_button_new();
+ } else {
+ /* Defaulting to type=text */
+ input = gtk_entry_new();
+ if (value != NULL) {
+ gtk_entry_set_text(GTK_ENTRY(input), value);
+ }
+ if (bs->current_form != NULL) {
+ g_signal_connect (input, "activate",
+ G_CALLBACK(form_submit), bs->current_form);
+ }
+ }
+ if (input != NULL) {
+ gtk_container_add (GTK_CONTAINER (bs->stack->data), input);
+ if (g_strcmp0(type, "hidden") != 0) {
+ gtk_widget_show_all(input);
+ }
+ }
+ if (input != NULL && bs->current_form != NULL && a_name != NULL) {
+ FormField *ff = malloc(sizeof(FormField));
+ ff->name = strdup(a_name);
+ ff->widget = input;
+ bs->current_form->fields = g_list_append(bs->current_form->fields, ff);
+ }
+ }
+ if (strcmp(name, "select") == 0) {
+ const gchar *a_name = NULL;
+ if (attrs != NULL) {
+ guint i;
+ for (i = 0; attrs[i]; i += 2){
+ if (g_strcmp0((const char*)attrs[i], "name") == 0) {
+ a_name = (const char*)attrs[i+1];
+ }
+ }
+ }
+ GtkWidget *cbox = gtk_combo_box_text_new();
+ gtk_container_add (GTK_CONTAINER (bs->stack->data), cbox);
+ bs->stack = g_slist_prepend(bs->stack, cbox);
+ gtk_widget_show_all(cbox);
+ if (bs->current_form != NULL && a_name != NULL) {
+ FormField *ff = malloc(sizeof(FormField));
+ ff->name = strdup(a_name);
+ ff->widget = cbox;
+ bs->current_form->fields = g_list_append(bs->current_form->fields, ff);
+ }
+ }
+
+ /* Links */
+ if (strcmp(name, "a") == 0) {
+ guint i;
+ const gchar *href = NULL;
+ if (attrs != NULL) {
+ for (i = 0; attrs[i]; i += 2){
+ if (strcmp((const char*)attrs[i], "href") == 0) {
+ href = (const char*)attrs[i+1];
+ }
+ }
+ }
+ if (href != NULL) {
+ bs->current_link = ib_link_new(href);
+
+ bs->current_link->start = bs->text_position;
+ INLINE_BOX(bs->stack->data)->links =
+ g_list_append(INLINE_BOX(bs->stack->data)->links, bs->current_link);
+ bs->docbox->links = g_list_append(bs->docbox->links, bs->current_link);
+ }
+ }
+ }
+ if (GTK_IS_COMBO_BOX_TEXT(bs->stack->data)) {
+ if (strcmp(name, "option") == 0) {
+ guint i;
+ if (attrs != NULL) {
+ for (i = 0; attrs[i]; i += 2) {
+ if (strcmp((const char*)attrs[i], "value") == 0) {
+ if (bs->option_value != NULL) {
+ free(bs->option_value);
+ }
+ bs->option_value = strdup((const char*)attrs[i+1]);
+ }
+ }
+ }
+ }
+ }
+
+
+ /* Formatting */
+ if (strcmp(name, "b") == 0 || strcmp(name, "strong") == 0) {
+ attribute_start(bs->current_attrs,
+ pango_attr_weight_new(PANGO_WEIGHT_BOLD),
+ (bs->current_word == NULL ? 0 : strlen(bs->current_word)));
+ } else if (strcmp(name, "i") == 0 || strcmp(name, "em") == 0) {
+ attribute_start(bs->current_attrs,
+ pango_attr_style_new(PANGO_STYLE_ITALIC),
+ (bs->current_word == NULL ? 0 : strlen(bs->current_word)));
+ } else if (strcmp(name, "code") == 0) {
+ attribute_start(bs->current_attrs,
+ pango_attr_family_new("mono"),
+ (bs->current_word == NULL ? 0 : strlen(bs->current_word)));
+ } else if (strcmp(name, "sub") == 0) {
+ /* todo: avoid using a constant */
+ attribute_start(bs->current_attrs,
+ pango_attr_rise_new(-5 * PANGO_SCALE),
+ (bs->current_word == NULL ? 0 : strlen(bs->current_word)));
+ attribute_start(bs->current_attrs,
+ pango_attr_scale_new(0.8),
+ (bs->current_word == NULL ? 0 : strlen(bs->current_word)));
+ } else if (strcmp(name, "sup") == 0) {
+ /* todo: avoid using a constant */
+ attribute_start(bs->current_attrs,
+ pango_attr_rise_new(5 * PANGO_SCALE),
+ (bs->current_word == NULL ? 0 : strlen(bs->current_word)));
+ attribute_start(bs->current_attrs,
+ pango_attr_scale_new(0.8),
+ (bs->current_word == NULL ? 0 : strlen(bs->current_word)));
+ } else if (strcmp(name, "h1") == 0) {
+ attribute_start(bs->current_attrs,
+ pango_attr_scale_new(1.8),
+ (bs->current_word == NULL ? 0 : strlen(bs->current_word)));
+ attribute_start(bs->current_attrs,
+ pango_attr_weight_new(PANGO_WEIGHT_SEMIBOLD),
+ (bs->current_word == NULL ? 0 : strlen(bs->current_word)));
+ } else if (strcmp(name, "h2") == 0) {
+ attribute_start(bs->current_attrs,
+ pango_attr_scale_new(1.6),
+ (bs->current_word == NULL ? 0 : strlen(bs->current_word)));
+ attribute_start(bs->current_attrs,
+ pango_attr_weight_new(PANGO_WEIGHT_SEMIBOLD),
+ (bs->current_word == NULL ? 0 : strlen(bs->current_word)));
+ } else if (strcmp(name, "h3") == 0) {
+ attribute_start(bs->current_attrs,
+ pango_attr_scale_new(1.4),
+ (bs->current_word == NULL ? 0 : strlen(bs->current_word)));
+ attribute_start(bs->current_attrs,
+ pango_attr_weight_new(PANGO_WEIGHT_SEMIBOLD),
+ (bs->current_word == NULL ? 0 : strlen(bs->current_word)));
+ } else if (strcmp(name, "h4") == 0) {
+ attribute_start(bs->current_attrs,
+ pango_attr_scale_new(1.3),
+ (bs->current_word == NULL ? 0 : strlen(bs->current_word)));
+ attribute_start(bs->current_attrs,
+ pango_attr_weight_new(PANGO_WEIGHT_SEMIBOLD),
+ (bs->current_word == NULL ? 0 : strlen(bs->current_word)));
+ } else if (strcmp(name, "h5") == 0) {
+ attribute_start(bs->current_attrs,
+ pango_attr_scale_new(1.2),
+ (bs->current_word == NULL ? 0 : strlen(bs->current_word)));
+ attribute_start(bs->current_attrs,
+ pango_attr_weight_new(PANGO_WEIGHT_SEMIBOLD),
+ (bs->current_word == NULL ? 0 : strlen(bs->current_word)));
+ } else if (strcmp(name, "h6") == 0) {
+ attribute_start(bs->current_attrs,
+ pango_attr_scale_new(1.1),
+ (bs->current_word == NULL ? 0 : strlen(bs->current_word)));
+ attribute_start(bs->current_attrs,
+ pango_attr_weight_new(PANGO_WEIGHT_SEMIBOLD),
+ (bs->current_word == NULL ? 0 : strlen(bs->current_word)));
+ } else if (strcmp(name, "a") == 0) {
+ attribute_start(bs->current_attrs,
+ pango_attr_foreground_new(bs->link_color.red * 65535,
+ bs->link_color.green * 65535,
+ bs->link_color.blue * 65535),
+ (bs->current_word == NULL ? 0 : strlen(bs->current_word)));
+ attribute_start(bs->current_attrs,
+ pango_attr_underline_new(PANGO_UNDERLINE_SINGLE),
+ (bs->current_word == NULL ? 0 : strlen(bs->current_word)));
+ }
+
+ /* Identifiers */
+ if (attrs != NULL) {
+ guint i;
+ for (i = 0; attrs[i]; i += 2){
+ if (strcmp((const char*)attrs[i], "id") == 0 ||
+ (strcmp(name, "a") == 0 &&
+ strcmp((const char*)attrs[i], "name") == 0)) {
+ bs->queued_identifiers =
+ g_slist_prepend(bs->queued_identifiers,
+ strdup((const char*)attrs[i + 1]));
+ }
+ }
+ }
+ if (bs->queued_identifiers && GTK_IS_IMAGE(bs->stack->data)) {
+ GSList *ii;
+ for (ii = bs->queued_identifiers; ii; ii = ii->next) {
+ /* todo: perhaps abstract this into a function, since it's the
+ same for texts. */
+ const char *fragment = soup_uri_get_fragment(bs->uri);
+ if (fragment && bs->anchor_handler_id == 0 &&
+ strcmp(ii->data, fragment) == 0) {
+ bs->anchor_handler_id =
+ g_signal_connect (bs->stack->data, "size-allocate",
+ G_CALLBACK(anchor_allocated), bs);
+ }
+ g_hash_table_insert(bs->identifiers, ii->data, bs->stack->data);
+ }
+ g_slist_free_full(bs->queued_identifiers, g_free);
+ bs->queued_identifiers = NULL;
+ }
+
+ /* Forms */
+ if (strcmp(name, "form") == 0) {
+ Form *form = malloc(sizeof(Form));
+ form->submission_data = (gpointer)bb;
+ form->method = NULL;
+ form->enctype = ENCTYPE_URLENCODED;
+ form->action = NULL;
+ form->fields = NULL;
+ guint i;
+ gchar *action = NULL;
+ if (attrs != NULL) {
+ for (i = 0; attrs[i]; i += 2) {
+ if (strcmp((const char*)attrs[i], "method") == 0) {
+ form->method = strdup((const char*)attrs[i+1]);
+ }
+ if (strcmp((const char*)attrs[i], "enctype") == 0) {
+ if (strcmp((const char*)attrs[i + 1], "multipart/form-data") == 0) {
+ form->enctype = ENCTYPE_MULTIPART;
+ } else if (strcmp((const char*)attrs[i + 1], "text/plain") == 0) {
+ form->enctype = ENCTYPE_PLAIN;
+ }
+ }
+ if (strcmp((const char*)attrs[i], "action") == 0) {
+ action = strdup((const char*)attrs[i+1]);
+ }
+ }
+ }
+ if (action == NULL) {
+ form->action = soup_uri_copy(bs->uri);
+ } else {
+ form->action = soup_uri_new_with_base(bs->uri, action);
+ }
+ bb->forms = g_list_prepend(bb->forms, form);
+ bs->current_form = form;
+ }
+}
+
+void sax_end_element (BrowserBox *bb, const xmlChar *u_name)
+{
+ BuilderState *bs = bb->builder_state;
+ const char *name = (const char*)u_name;
+
+ if (IS_INLINE_BOX(bs->stack->data)) {
+ if (element_flushes_text(name)) {
+ if (bs->current_word != NULL) {
+ add_word(bs, bs->current_word, &bs->current_attrs);
+ free(bs->current_word);
+ bs->current_word = NULL;
+ }
+ bs->prev_space = TRUE;
+ }
+ if (element_is_blocking(name)) {
+ GSList *next = bs->stack->next;
+ if (next != NULL) {
+ g_slist_free_1(bs->stack);
+ bs->stack = next;
+ }
+ bs->prev_space = TRUE;
+ }
+ }
+
+ if ((strcmp(name, "dl") == 0 || strcmp(name, "ul") == 0 ||
+ strcmp(name, "ol") == 0 || strcmp(name, "dd") == 0 ||
+ strcmp(name, "li") == 0 || strcmp(name, "select") == 0)) {
+ GSList *next = bs->stack->next;
+ if (next != NULL) {
+ g_slist_free_1(bs->stack);
+ bs->stack = next;
+ }
+ if (strcmp(name, "ol") == 0 || strcmp(name, "ul") == 0) {
+ GSList *next = bs->ol_numbers->next;
+ g_free(bs->ol_numbers->data);
+ g_slist_free_1(bs->ol_numbers);
+ bs->ol_numbers = next;
+ }
+ }
+ if (bs->stack && strcmp(name, "li") == 0) {
+ /* repeat */
+ GSList *next = bs->stack->next;
+ if (next != NULL) {
+ g_slist_free_1(bs->stack);
+ bs->stack = next;
+ }
+ }
+
+ if (strcmp(name, "option") == 0 && bs->option_value != NULL) {
+ free(bs->option_value);
+ bs->option_value = NULL;
+ }
+
+ /* Tables */
+ if (IS_TABLE_BOX(bs->stack->data)) {
+ if (strcmp(name, "table") == 0) {
+ GSList *next = bs->stack->next;
+ if (next != NULL) {
+ g_slist_free_1(bs->stack);
+ bs->stack = next;
+ }
+ }
+ }
+ if (IS_TABLE_CELL(bs->stack->data)) {
+ if (strcmp(name, "td") == 0 || strcmp(name, "th") == 0) {
+ GSList *next = bs->stack->next;
+ if (next != NULL) {
+ g_slist_free_1(bs->stack);
+ bs->stack = next;
+ }
+ }
+ }
+
+ /* Preformatted texts */
+ if (strcmp(name, "pre") == 0) {
+ bs->pre = FALSE;
+ bs->current_attrs = attribute_end(bs->current_attrs, PANGO_ATTR_FAMILY, 0);
+ }
+
+ /* Ignored */
+ if (strcmp(name, "head") == 0 || strcmp(name, "script") == 0 ||
+ strcmp(name, "style") == 0) {
+ bs->ignore_text = FALSE;
+ }
+
+ /* Formatting */
+ if (strcmp(name, "b") == 0 || strcmp(name, "strong") == 0) {
+ bs->current_attrs =
+ attribute_end(bs->current_attrs,
+ PANGO_ATTR_WEIGHT,
+ (bs->current_word == NULL ? 0 : strlen(bs->current_word)));
+ } else if (strcmp(name, "i") == 0 || strcmp(name, "em") == 0) {
+ bs->current_attrs =
+ attribute_end(bs->current_attrs,
+ PANGO_ATTR_STYLE,
+ (bs->current_word == NULL ? 0 : strlen(bs->current_word)));
+ } else if (strcmp(name, "code") == 0) {
+ bs->current_attrs =
+ attribute_end(bs->current_attrs,
+ PANGO_ATTR_FAMILY,
+ (bs->current_word == NULL ? 0 : strlen(bs->current_word)));
+ } else if (strcmp(name, "sub") == 0) {
+ bs->current_attrs =
+ attribute_end(bs->current_attrs,
+ PANGO_ATTR_RISE,
+ (bs->current_word == NULL ? 0 : strlen(bs->current_word)));
+ bs->current_attrs =
+ attribute_end(bs->current_attrs,
+ PANGO_ATTR_SCALE,
+ (bs->current_word == NULL ? 0 : strlen(bs->current_word)));
+ } else if (strcmp(name, "sup") == 0) {
+ bs->current_attrs =
+ attribute_end(bs->current_attrs,
+ PANGO_ATTR_RISE,
+ (bs->current_word == NULL ? 0 : strlen(bs->current_word)));
+ bs->current_attrs =
+ attribute_end(bs->current_attrs,
+ PANGO_ATTR_SCALE,
+ (bs->current_word == NULL ? 0 : strlen(bs->current_word)));
+ } else if (strcmp(name, "h1") == 0 || strcmp(name, "h2") == 0 ||
+ strcmp(name, "h3") == 0 || strcmp(name, "h4") == 0 ||
+ strcmp(name, "h5") == 0 || strcmp(name, "h6") == 0) {
+ bs->current_attrs =
+ attribute_end(bs->current_attrs,
+ PANGO_ATTR_SCALE,
+ (bs->current_word == NULL ? 0 : strlen(bs->current_word)));
+ bs->current_attrs =
+ attribute_end(bs->current_attrs,
+ PANGO_ATTR_WEIGHT,
+ (bs->current_word == NULL ? 0 : strlen(bs->current_word)));
+ } else if (strcmp(name, "a") == 0) {
+ bs->current_attrs =
+ attribute_end(bs->current_attrs,
+ PANGO_ATTR_FOREGROUND,
+ (bs->current_word == NULL ? 0 : strlen(bs->current_word)));
+ bs->current_attrs =
+ attribute_end(bs->current_attrs,
+ PANGO_ATTR_UNDERLINE,
+ (bs->current_word == NULL ? 0 : strlen(bs->current_word)));
+ if (bs->current_link != NULL) {
+ bs->current_link->end = bs->text_position;
+ bs->current_link = NULL;
+ }
+ }
+
+ /* Forms */
+ if (strcmp(name, "form") == 0) {
+ bs->current_form = NULL;
+ }
+}
+
+
+
+
+
+void select_text_cb (void *ptr,
+ gchar *str,
+ BrowserBox *bb)
+{
+ printf("Selection: '%s'\n", str);
+}
+
+
+void follow_link_cb (void *ptr,
+ gchar *url,
+ gboolean new_tab,
+ BrowserBox *bb)
+{
+ BuilderState *bs = bb->builder_state;
+ SoupURI *new_uri = soup_uri_new_with_base(bs->uri, url);
+ if (url[0] == '#') {
+ char *uri_str = soup_uri_to_string(new_uri, FALSE);
+ gtk_entry_set_text(GTK_ENTRY(bb->address_bar), uri_str);
+ free(uri_str);
+ soup_uri_free(new_uri);
+ scroll_to_identifier(bs, url + 1);
+ return;
+ }
+ BrowserBox *target_bb = bb;
+ if (new_tab) {
+ target_bb = browser_box_new(NULL);
+ gtk_widget_show_all(GTK_WIDGET(target_bb));
+ target_bb->tabs = bb->tabs;
+ gtk_stack_add_titled(GTK_STACK(target_bb->tabs), GTK_WIDGET(target_bb),
+ url, url);
+ }
+ history_add(target_bb, new_uri);
+ document_request(target_bb, new_uri);
+}
+
+void hover_link_cb (void *ptr,
+ gchar *url,
+ BrowserBox *bb)
+{
+ gtk_statusbar_remove_all(GTK_STATUSBAR(bb->status_bar),
+ gtk_statusbar_get_context_id(GTK_STATUSBAR(bb->status_bar), "status"));
+ gtk_statusbar_push(GTK_STATUSBAR(bb->status_bar),
+ gtk_statusbar_get_context_id(GTK_STATUSBAR(bb->status_bar), "status"),
+ url);
+}
+
+
+xmlSAXHandler sax = {
+ .characters = (charactersSAXFunc)sax_characters,
+ .startElement = (startElementSAXFunc)sax_start_element,
+ .endElement = (endElementSAXFunc)sax_end_element
+};
+
+void document_loaded(SoupSession *session,
+ SoupMessage *msg,
+ gpointer ptr)
+{
+ BrowserBox *bb = ptr;
+ BuilderState *bs = bb->builder_state;
+ if (! bs->active) {
+ return;
+ }
+ htmlParseChunk(bs->parser, "", 0, 1);
+ gtk_widget_grab_focus(GTK_WIDGET(bs->docbox));
+ printf("word cache: %u\n", g_hash_table_size(word_cache));
+ gtk_statusbar_remove_all(GTK_STATUSBAR(bb->status_bar),
+ gtk_statusbar_get_context_id(GTK_STATUSBAR(bb->status_bar), "status"));
+ gtk_statusbar_push(GTK_STATUSBAR(bb->status_bar),
+ gtk_statusbar_get_context_id(GTK_STATUSBAR(bb->status_bar), "status"),
+ "Ready");
+}
+
+void got_chunk(SoupMessage *msg,
+ SoupBuffer *chunk,
+ gpointer ptr)
+{
+ BrowserBox *bb = ptr;
+ BuilderState *bs = bb->builder_state;
+ gtk_statusbar_remove_all(GTK_STATUSBAR(bb->status_bar),
+ gtk_statusbar_get_context_id(GTK_STATUSBAR(bb->status_bar), "status"));
+ gtk_statusbar_push(GTK_STATUSBAR(bb->status_bar),
+ gtk_statusbar_get_context_id(GTK_STATUSBAR(bb->status_bar), "status"),
+ "Loading");
+ if (bs->parser == NULL) {
+ /* todo: maybe move it into got_headers */
+ char *uri_str = soup_uri_to_string(bs->uri, FALSE);
+ bs->parser =
+ htmlCreatePushParserCtxt(&sax, bb, "", 0, uri_str,
+ XML_CHAR_ENCODING_UTF8);
+ free(uri_str);
+ bs->docbox = document_box_new();
+ gtk_container_add (GTK_CONTAINER (bs->root), GTK_WIDGET (bs->docbox));
+ bs->vbox = block_box_new(10);
+ bs->stack->data = bs->vbox;
+ gtk_container_add(GTK_CONTAINER (DOCUMENT_BOX(bs->docbox)->evbox),
+ GTK_WIDGET (bs->vbox));
+ g_signal_connect (bs->docbox, "follow", G_CALLBACK(follow_link_cb), bb);
+ g_signal_connect (bs->docbox, "hover", G_CALLBACK(hover_link_cb), bb);
+ g_signal_connect (bs->docbox, "select", G_CALLBACK(select_text_cb), bb);
+ gtk_widget_show_all(GTK_WIDGET(bs->docbox));
+ gtk_box_set_child_packing(GTK_BOX(bs->root), GTK_WIDGET(bs->docbox),
+ TRUE, TRUE, 0, GTK_PACK_END);
+ }
+ if (bs->active) {
+ htmlParseChunk(bs->parser, chunk->data, chunk->length, 0);
+ }
+ return;
+}
+
+void got_headers(SoupMessage *msg, gpointer ptr)
+{
+ BrowserBox *bb = ptr;
+ gtk_statusbar_remove_all(GTK_STATUSBAR(bb->status_bar),
+ gtk_statusbar_get_context_id(GTK_STATUSBAR(bb->status_bar), "status"));
+ gtk_statusbar_push(GTK_STATUSBAR(bb->status_bar),
+ gtk_statusbar_get_context_id(GTK_STATUSBAR(bb->status_bar), "status"),
+ "Got headers");
+ /* todo: check content type, don't assume HTML */
+ if (bb->builder_state != NULL) {
+ if (bb->builder_state->docbox != NULL) {
+ gtk_widget_destroy(GTK_WIDGET(bb->builder_state->docbox));
+ }
+ g_object_unref(bb->builder_state);
+ }
+ bb->builder_state = builder_state_new(bb->docbox_root);
+ bb->builder_state->uri = soup_uri_copy(soup_message_get_uri(msg));
+ char *uri_str = soup_uri_to_string(bb->builder_state->uri, FALSE);
+ gtk_entry_set_text(GTK_ENTRY(bb->address_bar), uri_str);
+ free(uri_str);
+}
+
+void document_request_sm (BrowserBox *bb, SoupMessage *sm)
+{
+ gtk_statusbar_remove_all(GTK_STATUSBAR(bb->status_bar),
+ gtk_statusbar_get_context_id(GTK_STATUSBAR(bb->status_bar), "status"));
+ gtk_statusbar_push(GTK_STATUSBAR(bb->status_bar),
+ gtk_statusbar_get_context_id(GTK_STATUSBAR(bb->status_bar), "status"),
+ "Requesting");
+ if (bb->builder_state != NULL) {
+ bb->builder_state->active = FALSE;
+ }
+ soup_session_abort(bb->soup_session);
+ g_signal_connect (sm, "got-chunk", (GCallback)got_chunk, bb);
+ g_signal_connect (sm, "got-headers", (GCallback)got_headers, bb);
+ soup_session_queue_message(bb->soup_session, sm,
+ (SoupSessionCallback)document_loaded, bb);
+}
+
+void document_request (BrowserBox *bb, SoupURI *uri)
+{
+ SoupMessage *sm = soup_message_new_from_uri("GET", uri);
+ document_request_sm(bb, sm);
+}
+
+
+void address_bar_activate (GtkEntry *ab, BrowserBox *bb)
+{
+ SoupURI *uri = soup_uri_new(gtk_entry_get_text(ab));
+ if (uri) {
+ history_add(bb, uri);
+ document_request(bb, uri);
+ }
+}
+
+
+
+static void browser_box_dispose (GObject *object) {
+ BrowserBox *bb = BROWSER_BOX(object);
+ GList *form_iter;
+ if (bb->forms != NULL) {
+ for (form_iter = bb->forms; form_iter; form_iter = form_iter->next) {
+ Form *form = form_iter->data;
+ if (form->method != NULL) {
+ free(form->method);
+ form->method = NULL;
+ }
+ if (form->action != NULL) {
+ soup_uri_free(form->action);
+ form->action = NULL;
+ }
+ if (form->fields != NULL) {
+ GList *field_iter;
+ for (field_iter = form->fields; field_iter; field_iter = field_iter->next) {
+ FormField *field = field_iter->data;
+ if (field->name != NULL) {
+ free(field->name);
+ field->name = NULL;
+ }
+ free(field);
+ }
+ g_list_free(form->fields);
+ form->fields = NULL;
+ }
+ free(form);
+ }
+ g_list_free(bb->forms);
+ bb->forms = NULL;
+ }
+ if (bb->history != NULL) {
+ g_list_free_full(bb->history, (GDestroyNotify)soup_uri_free);
+ bb->history = NULL;
+ bb->history_position = NULL;
+ }
+ G_OBJECT_CLASS (browser_box_parent_class)->dispose(object);
+}
+
+static void
+browser_box_class_init (BrowserBoxClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ object_class->dispose = browser_box_dispose;
+ return;
+}
+
+static void
+browser_box_init (BrowserBox *bb)
+{
+ bb->builder_state = NULL;
+ bb->forms = NULL;
+ bb->history = NULL;
+ bb->history_position = NULL;
+ return;
+}
+
+void document_request (BrowserBox *bb, SoupURI *uri);
+
+
+BrowserBox *browser_box_new (gchar *uri_str)
+{
+ BrowserBox *bb = BROWSER_BOX(g_object_new(browser_box_get_type(),
+ "orientation", GTK_ORIENTATION_VERTICAL,
+ "spacing", 0,
+ NULL));
+ bb->address_bar = gtk_entry_new();
+ gtk_container_add (GTK_CONTAINER(bb), bb->address_bar);
+ g_signal_connect(bb->address_bar, "activate",
+ (GCallback)address_bar_activate, bb);
+
+ bb->docbox_root = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
+ gtk_container_add (GTK_CONTAINER(bb), bb->docbox_root);
+ gtk_box_set_child_packing(GTK_BOX(bb), bb->docbox_root, TRUE, TRUE, 0, GTK_PACK_START);
+
+ bb->status_bar = gtk_statusbar_new();
+ gtk_container_add (GTK_CONTAINER(bb), bb->status_bar);
+
+ bb->soup_session =
+ soup_session_new_with_options("user-agent", "WWWLite/0.0.0", NULL);
+
+ /* bb->word_cache = g_hash_table_new((GHashFunc)wck_hash, (GEqualFunc)wck_equal); */
+
+ if (uri_str) {
+ SoupURI *uri = soup_uri_new(uri_str);
+ history_add(bb, uri);
+ document_request(bb, soup_uri_new(uri_str));
+ }
+
+ return bb;
+}
diff --git a/src/browserbox.h b/src/browserbox.h
new file mode 100644
index 0000000..5016c67
--- /dev/null
+++ b/src/browserbox.h
@@ -0,0 +1,133 @@
+/* WWWLite, a lightweight web browser.
+ Copyright (C) 2019 defanor
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+*/
+
+#ifndef BROWSER_BOX_H
+#define BROWSER_BOX_H
+
+#include <gtk/gtk.h>
+#include <libsoup/soup.h>
+#include "documentbox.h"
+#include "inlinebox.h"
+#include "blockbox.h"
+#include <libxml/HTMLparser.h>
+
+G_BEGIN_DECLS
+
+typedef struct _FormField FormField;
+struct _FormField
+{
+ gchar *name;
+ GtkWidget *widget;
+};
+
+enum {
+ ENCTYPE_URLENCODED,
+ ENCTYPE_MULTIPART,
+ ENCTYPE_PLAIN
+};
+
+typedef struct _Form Form;
+struct _Form
+{
+ gchar *method;
+ int enctype;
+ SoupURI *action;
+ GList *fields;
+ gpointer *submission_data;
+};
+
+#define BUILDER_STATE_TYPE (builder_state_get_type())
+G_DECLARE_FINAL_TYPE (BuilderState, builder_state, BUILDER, STATE, GObject);
+
+struct _BuilderState
+{
+ GObject parent_instance;
+ gboolean active;
+ GtkWidget *root;
+ DocumentBox *docbox;
+ GtkWidget *vbox;
+ GSList *stack;
+ GdkRGBA link_color;
+ guint text_position;
+ PangoAttrList *current_attrs;
+ IBLink *current_link;
+ gchar *current_word;
+ gboolean ignore_text;
+ gboolean prev_space;
+ gboolean pre;
+ htmlParserCtxtPtr parser;
+ SoupURI *uri;
+ GSList *queued_identifiers;
+ GHashTable *identifiers;
+ gulong anchor_handler_id;
+ gchar *option_value;
+ GSList *ol_numbers;
+ Form *current_form;
+};
+
+#define BROWSER_BOX_TYPE (browser_box_get_type())
+#define BROWSER_BOX(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), BROWSER_BOX_TYPE, BrowserBox))
+#define BROWSER_BOX_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), BROWSER_BOX_TYPE, BrowserBoxClass))
+#define IS_BROWSER_BOX(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), BROWSER_BOX_TYPE))
+#define IS_BROWSER_BOX_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), BROWSER_BOX_TYPE))
+
+
+typedef struct _BrowserBox BrowserBox;
+typedef struct _BrowserBoxClass BrowserBoxClass;
+
+struct _BrowserBox
+{
+ BlockBox parent_instance;
+ SoupSession *soup_session;
+ BuilderState *builder_state;
+ GtkWidget *address_bar;
+ GtkWidget *docbox_root;
+ GtkWidget *status_bar;
+ GList *forms;
+ GList *history;
+ GList *history_position;
+ GtkStack *tabs;
+ /* GHashTable *word_cache; */
+};
+
+struct _BrowserBoxClass
+{
+ BlockBoxClass parent_class;
+};
+
+GType browser_box_get_type(void) G_GNUC_CONST;
+BrowserBox *browser_box_new(gchar *uri_str);
+
+typedef struct _WordCacheKey WordCacheKey;
+struct _WordCacheKey
+{
+ gchar *text;
+ PangoAttrList *attrs;
+};
+guint wck_hash (WordCacheKey *wck);
+gboolean wck_equal (WordCacheKey *wck1, WordCacheKey *wck2);
+
+void document_request_sm (BrowserBox *bb, SoupMessage *sm);
+void document_request (BrowserBox *bb, SoupURI *uri);
+gboolean history_back (BrowserBox *bb);
+gboolean history_forward (BrowserBox *bb);
+
+GHashTable *word_cache;
+
+G_END_DECLS
+
+#endif
diff --git a/src/documentbox.c b/src/documentbox.c
new file mode 100644
index 0000000..b506385
--- /dev/null
+++ b/src/documentbox.c
@@ -0,0 +1,489 @@
+/* WWWLite, a lightweight web browser.
+ Copyright (C) 2019 defanor
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+*/
+
+#include <gtk/gtk.h>
+#include "documentbox.h"
+#include "inlinebox.h"
+
+G_DEFINE_TYPE (DocumentBox, document_box, GTK_TYPE_SCROLLED_WINDOW);
+
+
+static void document_box_dispose (GObject *object) {
+ DocumentBox *db = DOCUMENT_BOX(object);
+ if (db->links != NULL) {
+ /* The same links are also referenced from InlineBox, and freed on
+ its disposal, so only the list needs to be freed here. */
+ g_list_free(db->links);
+ db->links = NULL;
+ }
+ G_OBJECT_CLASS (document_box_parent_class)->dispose (object);
+}
+
+enum {
+ FOLLOW,
+ SELECT,
+ HOVER
+};
+
+static guint signals[3];
+
+static GtkSizeRequestMode document_box_get_request_mode (GtkWidget *widget)
+{
+ return GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH;
+}
+
+static void
+document_box_class_init (DocumentBoxClass *klass)
+{
+ GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass);
+ /* GtkContainerClass *container_class = GTK_CONTAINER_CLASS(klass); */
+ signals[FOLLOW] =
+ g_signal_new("follow",
+ G_TYPE_FROM_CLASS (gobject_class),
+ G_SIGNAL_RUN_LAST,
+ 0, /* class_offset */
+ NULL, /* accumulator */
+ NULL, /* accu_data */
+ NULL, /* c_marshaller */
+ G_TYPE_NONE, /* return_type */
+ 2, /* n_params */
+ G_TYPE_STRING,
+ G_TYPE_BOOLEAN);
+ signals[SELECT] =
+ g_signal_new("select",
+ G_TYPE_FROM_CLASS (gobject_class),
+ G_SIGNAL_RUN_LAST,
+ 0, /* class_offset */
+ NULL, /* accumulator */
+ NULL, /* accu_data */
+ NULL, /* c_marshaller */
+ G_TYPE_NONE, /* return_type */
+ 1, /* n_params */
+ G_TYPE_STRING);
+ signals[HOVER] =
+ g_signal_new("hover",
+ G_TYPE_FROM_CLASS (gobject_class),
+ G_SIGNAL_RUN_LAST,
+ 0, /* class_offset */
+ NULL, /* accumulator */
+ NULL, /* accu_data */
+ NULL, /* c_marshaller */
+ G_TYPE_NONE, /* return_type */
+ 1, /* n_params */
+ G_TYPE_STRING);
+ widget_class->get_request_mode = document_box_get_request_mode;
+ gobject_class->dispose = document_box_dispose;
+ return;
+}
+
+static void
+document_box_init (DocumentBox *db)
+{
+ db->links = NULL;
+}
+
+
+typedef struct _SearchState SearchState;
+struct _SearchState
+{
+ gint x;
+ gint y;
+ guint index;
+ IBText *ibt;
+ InlineBox *ib;
+ guint ib_index;
+};
+
+static void
+text_at_position(GtkWidget *widget, SearchState *st)
+{
+ GtkAllocation alloc;
+ gtk_widget_get_allocation(widget, &alloc);
+ if (st->x >= alloc.x &&
+ st->x <= alloc.x + alloc.width &&
+ st->y >= alloc.y &&
+ st->y <= alloc.y + alloc.height) {
+ if (IS_INLINE_BOX(widget)) {
+ st->ib = INLINE_BOX(widget);
+ GList *ti;
+ guint text_position = 0;
+ for (ti = INLINE_BOX(widget)->children; ti; ti = ti->next) {
+ if (IS_IB_TEXT(ti->data)) {
+ IBText *ibt = IB_TEXT(ti->data);
+ if (st->x >= ibt->alloc.x &&
+ st->x <= ibt->alloc.x + ibt->alloc.width &&
+ st->y >= ibt->alloc.y &&
+ st->y <= ibt->alloc.y + ibt->alloc.height) {
+ gint position;
+ pango_layout_xy_to_index(ibt->layout,
+ (st->x - ibt->alloc.x) * PANGO_SCALE,
+ (st->y - ibt->alloc.y) * PANGO_SCALE,
+ &position,
+ NULL);
+ st->index = position;
+ st->ibt = ibt;
+ st->ib_index = text_position + position;
+ return;
+ }
+ text_position += strlen(pango_layout_get_text(ibt->layout));
+ }
+ }
+ return;
+ } else if (GTK_IS_CONTAINER(widget)) {
+ gtk_container_foreach(GTK_CONTAINER(widget),
+ (GtkCallback)text_at_position, st);
+ }
+ }
+}
+
+static gboolean widget_is_affected(GtkAllocation *wa, GtkAllocation *ta1,
+ GtkAllocation *ta2)
+{
+ if (wa == NULL || ta1 == NULL || ta2 == NULL) {
+ return FALSE;
+ }
+ return
+ gdk_rectangle_intersect(wa, ta1, NULL) ||
+ gdk_rectangle_intersect(wa, ta2, NULL) ||
+ ((ta2->y >= ta1->y &&
+ ((wa->y >= ta1->y) && (wa->y <= ta2->y))) ||
+ ((ta2->y <= ta1->y) &&
+ ((wa->y >= ta2->y) && (wa->y <= ta1->y))));
+}
+
+/* Assuming (Y, X) is strictly increasing */
+static gint compare_positions(GtkAllocation *a1, guint i1,
+ GtkAllocation *a2, guint i2)
+{
+ if ((a1->y < a2->y) ||
+ (a1->y == a2->y && a1->x < a2->x) ||
+ (a1->y == a2->y && a1->x == a2->x && i1 < i2)) {
+ return -1;
+ } else if (a1->y == a2->y && a1->x == a2->x && i1 == i2) {
+ return 0;
+ } else {
+ return 1;
+ }
+}
+
+
+static void
+selection_update (GtkWidget *widget, SelectionState *st)
+{
+ GtkAllocation alloc;
+ gtk_widget_get_allocation(widget, &alloc);
+ if (widget_is_affected(&alloc, &st->selection_start->alloc,
+ &st->selection_end->alloc) ||
+ widget_is_affected(&alloc, &st->selection_start->alloc,
+ &st->selection_prev->alloc) ||
+ widget_is_affected(&alloc, &st->selection_end->alloc,
+ &st->selection_prev->alloc)) {
+ if (IS_INLINE_BOX(widget)) {
+ InlineBox *ib = INLINE_BOX(widget);
+ ib->selection_end = 0;
+ ib->selection_start = 0;
+ GList *ti;
+ guint text_position = 0;
+
+ for (ti = ib->children; ti; ti = ti->next) {
+ if (IS_IB_TEXT(ti->data)) {
+ IBText *ibt = IB_TEXT(ti->data);
+ gint direction = compare_positions(&st->selection_start->alloc,
+ st->selection_start_index,
+ &st->selection_end->alloc,
+ st->selection_end_index);
+ if (direction == -1) {
+ if (st->selection_start == ibt) {
+ ib->selection_start = st->selection_start_index + text_position;
+ st->selecting = TRUE;
+ }
+ if (st->selecting && st->selection_end == ibt) {
+ ib->selection_end = st->selection_end_index + text_position;
+ st->selecting = FALSE;
+ }
+ } else if (direction == 1) {
+ if (st->selection_end == ibt) {
+ ib->selection_start = st->selection_end_index + text_position;
+ st->selecting = TRUE;
+ }
+ if (st->selecting && st->selection_start == ibt) {
+ ib->selection_end = st->selection_start_index + text_position;
+ st->selecting = FALSE;
+ }
+ }
+ text_position += strlen(pango_layout_get_text(ibt->layout));
+ gtk_widget_queue_draw (widget);
+ }
+ }
+ if (st->selecting) {
+ ib->selection_end = text_position;
+ }
+ } else if (GTK_IS_CONTAINER(widget)) {
+ gtk_container_foreach(GTK_CONTAINER(widget),
+ (GtkCallback)selection_update, st);
+ }
+ }
+}
+
+static void
+selection_read (GtkWidget *widget, gchar **str)
+{
+ if (IS_INLINE_BOX(widget)) {
+ InlineBox *ib = INLINE_BOX(widget);
+ if (ib->selection_end == 0) {
+ return;
+ }
+ GList *ti;
+ guint text_position = 0;
+ gboolean affected = FALSE, breaks = FALSE;
+ for (ti = ib->children; ti; ti = ti->next) {
+ if (IS_IB_TEXT(ti->data)) {
+ IBText *ibt = IB_TEXT(ti->data);
+ const gchar *word = pango_layout_get_text(ibt->layout);
+ guint word_len = strlen(word);
+ if (ib->selection_start <= text_position + word_len &&
+ ib->selection_end > text_position) {
+ guint start_offset = 0, end_offset = 0;
+ if (ib->selection_start > text_position) {
+ start_offset = ib->selection_start - text_position;
+ }
+ if (ib->selection_end < text_position + word_len) {
+ end_offset = text_position + word_len - ib->selection_end;
+ }
+ guint len = word_len - start_offset - end_offset;
+ *str = realloc(*str, strlen(*str) + len + 1);
+ g_strlcpy(*str + strlen(*str), word + start_offset, len + 1);
+ affected = TRUE;
+ breaks = TRUE;
+ } else {
+ breaks = FALSE;
+ }
+ text_position += word_len;
+ } else if (breaks && IS_IB_BREAK(ti->data)) {
+ *str = realloc(*str, strlen(*str) + 2);
+ (*str)[strlen(*str) + 1] = 0;
+ (*str)[strlen(*str)] = '\n';
+ breaks = FALSE;
+ }
+ }
+ if (affected) {
+ /* Add one more newline in the end, so that there are newlines
+ between paragraphs. */
+ *str = realloc(*str, strlen(*str) + 2);
+ (*str)[strlen(*str) + 1] = 0;
+ (*str)[strlen(*str)] = '\n';
+ }
+ } else if (GTK_IS_CONTAINER(widget)) {
+ gtk_container_foreach(GTK_CONTAINER(widget),
+ (GtkCallback)selection_read, str);
+ }
+}
+
+static IBLink *find_link (SearchState *ss, gint x, gint y)
+{
+ if (ss->ib && ss->ib->links != NULL) {
+ GList *li;
+ for (li = ss->ib->links; li; li = li->next) {
+ IBLink *link = IB_LINK(li->data);
+ if (ss->ibt) {
+ if (link->start <= ss->ib_index && link->end > ss->ib_index) {
+ return link;
+ }
+ }
+ GList *oi;
+ GtkAllocation alloc;
+ for (oi = link->objects; oi; oi = oi->next) {
+ gtk_widget_get_allocation(oi->data, &alloc);
+ if (alloc.x <= x && alloc.x + alloc.width >= x &&
+ alloc.y <= y && alloc.y + alloc.height >= y) {
+ return link;
+ }
+ }
+ }
+ }
+ return NULL;
+}
+
+static gboolean
+button_press_event_cb (GtkWidget *widget,
+ GdkEventButton *event,
+ DocumentBox *db)
+{
+ if (event->button != 1) {
+ return FALSE;
+ }
+ SearchState ss;
+ gint orig_x, orig_y, ev_orig_x, ev_orig_y;
+ gdk_window_get_origin(gtk_widget_get_parent_window(GTK_WIDGET(db->evbox)),
+ &orig_x, &orig_y);
+ gdk_window_get_origin(event->window, &ev_orig_x, &ev_orig_y);
+ ss.ibt = NULL;
+ ss.x = ev_orig_x - orig_x + event->x;
+ ss.y = ev_orig_y - orig_y + event->y;
+ text_at_position(widget, &ss);
+
+ if (db->sel.selection_end) {
+ /* Remove existing selection */
+ db->sel.selection_prev = db->sel.selection_end;
+ db->sel.selection_prev_index = db->sel.selection_end_index + 1;
+ db->sel.selection_end = db->sel.selection_start;
+ db->sel.selection_end_index = db->sel.selection_start_index;
+ selection_update(widget, &db->sel);
+ }
+
+ if (ss.ibt) {
+ db->sel.selection_active = TRUE;
+ db->sel.selection_start = ss.ibt;
+ db->sel.selection_start_index = ss.index;
+ db->sel.selection_end = ss.ibt;
+ db->sel.selection_end_index = ss.index;
+ /* todo: grab focus when any non-widget space is clicked, not
+ just texts */
+ gtk_widget_grab_focus(GTK_WIDGET(db));
+ }
+ return FALSE;
+}
+
+static gboolean
+motion_notify_event_cb (GtkWidget *widget,
+ GdkEventButton *event,
+ DocumentBox *db)
+{
+ SearchState ss;
+ gint orig_x, orig_y, ev_orig_x, ev_orig_y;
+ gdk_window_get_origin(gtk_widget_get_parent_window(GTK_WIDGET(db->evbox)),
+ &orig_x, &orig_y);
+ gdk_window_get_origin(event->window, &ev_orig_x, &ev_orig_y);
+ ss.ib = NULL;
+ ss.ibt = NULL;
+ ss.x = ev_orig_x - orig_x + event->x;
+ ss.y = ev_orig_y - orig_y + event->y;
+ text_at_position(widget, &ss);
+ if (ss.ibt && db->sel.selection_active) {
+ db->sel.selection_prev = db->sel.selection_end;
+ db->sel.selection_prev_index = db->sel.selection_end_index;
+ db->sel.selection_end = ss.ibt;
+ db->sel.selection_end_index = ss.index;
+ db->sel.selecting = FALSE;
+ selection_update(widget, &db->sel);
+ }
+ IBLink *link = find_link(&ss, event->x, event->y);
+ if (link != NULL) {
+ g_signal_emit(db, signals[HOVER], 0, link->url);
+ }
+ return FALSE;
+}
+
+
+static gboolean
+button_release_event_cb (GtkWidget *widget,
+ GdkEventButton *event,
+ DocumentBox *db)
+{
+ if (event->button != 1 && event->button != 2) {
+ return FALSE;
+ }
+ gchar *str = malloc(1);
+ gboolean got_selection = FALSE;
+ str[0] = 0;
+ selection_read(widget, &str);
+ if (strlen(str) > 0) {
+ /* Strip the last newline */
+ str[strlen(str) - 1] = 0;
+ got_selection = TRUE;
+ }
+ g_signal_emit(db, signals[SELECT], 0, str);
+ g_free(str);
+ db->sel.selection_active = FALSE;
+ if (got_selection) {
+ return FALSE;
+ }
+
+ SearchState ss;
+ gint orig_x, orig_y, ev_orig_x, ev_orig_y;
+ gdk_window_get_origin(gtk_widget_get_parent_window(GTK_WIDGET(db->evbox)),
+ &orig_x, &orig_y);
+ gdk_window_get_origin(event->window, &ev_orig_x, &ev_orig_y);
+ ss.ib = NULL;
+ ss.ibt = NULL;
+ ss.x = ev_orig_x - orig_x + event->x;
+ ss.y = ev_orig_y - orig_y + event->y;
+ text_at_position(widget, &ss);
+
+ IBLink *link = find_link(&ss, event->x, event->y);
+ if (link != NULL) {
+ g_signal_emit(db, signals[FOLLOW], 0, link->url, event->button == 2);
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static void
+find_focused (GtkWidget *widget, GObject **focused) {
+ if (IS_INLINE_BOX(widget)) {
+ if (INLINE_BOX(widget)->focused_object != NULL) {
+ *focused = INLINE_BOX(widget)->focused_object;
+ return;
+ }
+ } else if (GTK_IS_CONTAINER(widget)) {
+ gtk_container_foreach(GTK_CONTAINER(widget),
+ (GtkCallback)find_focused, focused);
+ }
+}
+
+
+static gboolean
+key_press_event_cb (GtkWidget *widget, GdkEventKey *event, DocumentBox *db)
+{
+ if (event->keyval == GDK_KEY_Return) {
+ GObject *focused;
+ /* This is inefficient and can be optimised, but there's no
+ perceivable delay, so perhaps it doesn't worth adding more
+ code. */
+ find_focused(widget, &focused);
+ if (focused != NULL && IS_IB_LINK(focused)) {
+ g_signal_emit(db, signals[FOLLOW], 0, IB_LINK(focused)->url,
+ event->state & GDK_CONTROL_MASK);
+ return TRUE;
+ }
+ }
+ return FALSE;
+}
+
+DocumentBox *document_box_new ()
+{
+ DocumentBox *db = DOCUMENT_BOX(g_object_new(document_box_get_type(),
+ "hadjustment", NULL,
+ "vadjustment", NULL,
+ NULL));
+ db->evbox = GTK_EVENT_BOX(gtk_event_box_new());
+ gtk_widget_add_events(GTK_WIDGET(db->evbox), GDK_POINTER_MOTION_MASK);
+ gtk_container_add (GTK_CONTAINER (db), GTK_WIDGET (db->evbox));
+
+ g_signal_connect (db->evbox, "button-press-event",
+ G_CALLBACK (button_press_event_cb), db);
+ g_signal_connect (db->evbox, "button-release-event",
+ G_CALLBACK (button_release_event_cb), db);
+ g_signal_connect (db->evbox, "motion-notify-event",
+ G_CALLBACK (motion_notify_event_cb), db);
+ g_signal_connect (db->evbox, "key-press-event",
+ G_CALLBACK (key_press_event_cb), db);
+ db->links = NULL;
+ db->sel.selection_active = FALSE;
+ return db;
+}
diff --git a/src/documentbox.h b/src/documentbox.h
new file mode 100644
index 0000000..351471b
--- /dev/null
+++ b/src/documentbox.h
@@ -0,0 +1,70 @@
+/* WWWLite, a lightweight web browser.
+ Copyright (C) 2019 defanor
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+*/
+
+#ifndef DOCUMENT_BOX_H
+#define DOCUMENT_BOX_H
+
+#include <gtk/gtk.h>
+#include "inlinebox.h"
+#include <libsoup/soup.h>
+
+G_BEGIN_DECLS
+
+#define DOCUMENT_BOX_TYPE (document_box_get_type())
+#define DOCUMENT_BOX(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), DOCUMENT_BOX_TYPE, DocumentBox))
+#define DOCUMENT_BOX_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), DOCUMENT_BOX_TYPE, DocumentBoxClass))
+#define IS_DOCUMENT_BOX(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), DOCUMENT_BOX_TYPE))
+#define IS_DOCUMENT_BOX_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), DOCUMENT_BOX_TYPE))
+
+typedef struct _DocumentBox DocumentBox;
+typedef struct _DocumentBoxClass DocumentBoxClass;
+
+typedef struct _SelectionState SelectionState;
+struct _SelectionState
+{
+ IBText *selection_start;
+ guint selection_start_index;
+ IBText *selection_end;
+ guint selection_end_index;
+ IBText *selection_prev;
+ guint selection_prev_index;
+ gboolean selection_active;
+ gboolean selecting;
+};
+
+struct _DocumentBox
+{
+ GtkScrolledWindow parent_instance;
+ GtkEventBox *evbox;
+ GList *links;
+ SelectionState sel;
+ GdkWindow *event_window;
+ /* GList *forms; */
+};
+
+struct _DocumentBoxClass
+{
+ GtkScrolledWindowClass parent_class;
+};
+
+GType document_box_get_type(void) G_GNUC_CONST;
+DocumentBox *document_box_new(void);
+
+
+G_END_DECLS
+
+#endif
diff --git a/src/inlinebox.c b/src/inlinebox.c
new file mode 100644
index 0000000..43b75fa
--- /dev/null
+++ b/src/inlinebox.c
@@ -0,0 +1,585 @@
+/* WWWLite, a lightweight web browser.
+ Copyright (C) 2019 defanor
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+*/
+
+#include <gtk/gtk.h>
+#include "inlinebox.h"
+
+
+static GtkSizeRequestMode inline_box_get_request_mode (GtkWidget *widget);
+static void inline_box_get_preferred_width(GtkWidget *widget,
+ gint *minimal, gint *natural);
+static void inline_box_get_preferred_height_for_width(GtkWidget *widget,
+ gint width,
+ gint *minimal,
+ gint *natural);
+static void inline_box_size_allocate(GtkWidget *widget,
+ GtkAllocation *allocation);
+static GType inline_box_child_type(GtkContainer *container);
+static void inline_box_add(GtkContainer *container, GtkWidget *widget);
+static void inline_box_remove(GtkContainer *container, GtkWidget *widget);
+static void inline_box_forall(GtkContainer *container,
+ gboolean include_internals,
+ GtkCallback callback, gpointer callback_data);
+static void inline_box_dispose (GObject *object);
+static void inline_box_finalize (GObject *object);
+static void ib_text_dispose (GObject *self);
+static void ib_link_dispose (GObject *self);
+
+
+static void ib_text_class_init (IBTextClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ object_class->dispose = ib_text_dispose;
+}
+
+static void ib_text_init (IBText *self)
+{
+ self->layout = NULL;
+}
+
+static void ib_text_dispose (GObject *self)
+{
+ IBText *ibt = IB_TEXT(self);
+ g_clear_object(&ibt->layout);
+}
+
+IBText *ib_text_new (PangoLayout *layout)
+{
+ IBText *ib_text = g_object_new (IB_TEXT_TYPE, NULL);
+ PangoRectangle extents;
+ pango_layout_get_pixel_extents(layout, NULL, &extents);
+ ib_text->alloc.x = 0;
+ ib_text->alloc.y = 0;
+ ib_text->alloc.width = extents.width;
+ ib_text->alloc.height = extents.height;
+ ib_text->layout = layout;
+ g_object_ref(layout);
+ return IB_TEXT (ib_text);
+}
+
+static void ib_link_class_init (IBLinkClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ object_class->dispose = ib_link_dispose;
+}
+
+static void ib_link_init (IBLink *self)
+{
+ self->url = NULL;
+ self->objects = NULL;
+}
+
+static void ib_link_dispose (GObject *self)
+{
+ IBLink *ibl = IB_LINK(self);
+ g_free(ibl->url);
+ ibl->url = NULL;
+}
+
+IBLink *ib_link_new (const gchar *url)
+{
+ IBLink *ib_link = g_object_new (IB_LINK_TYPE, NULL);
+ ib_link->url = strdup(url);
+ return ib_link;
+}
+
+
+static void ib_break_class_init (IBBreakClass *klass)
+{
+ return;
+}
+
+static void ib_break_init (IBBreak *self)
+{
+ return;
+}
+
+IBBreak *ib_break_new ()
+{
+ return g_object_new (IB_BREAK_TYPE, NULL);
+}
+
+
+G_DEFINE_TYPE (IBLink, ib_link, G_TYPE_OBJECT);
+G_DEFINE_TYPE (IBBreak, ib_break, G_TYPE_OBJECT);
+G_DEFINE_TYPE (IBText, ib_text, G_TYPE_OBJECT);
+G_DEFINE_TYPE (InlineBox, inline_box, GTK_TYPE_CONTAINER);
+
+
+static gint
+inline_box_draw (GtkWidget *widget,
+ cairo_t *cr)
+{
+ GList *child;
+ InlineBox *ib = INLINE_BOX(widget);
+ guint text_position = 0;
+ for (child = ib->children; child; child = child->next) {
+ if (GTK_IS_WIDGET(child->data)) {
+ gtk_container_propagate_draw((GTK_CONTAINER(widget)),
+ GTK_WIDGET(child->data), cr);
+ /* todo: render focus around widgets (images in particular)
+ too */
+ } else if (IS_IB_TEXT(child->data)) {
+ IBText *ibt = IB_TEXT(child->data);
+ GtkAllocation alloc;
+ GtkStyleContext *styleCtx = gtk_widget_get_style_context(widget);
+ guint text_len = strlen(pango_layout_get_text(ibt->layout));
+ gtk_widget_get_allocation (widget, &alloc);
+ cairo_translate (cr, -alloc.x, -alloc.y);
+
+ if (ib->selection_start <= text_position + text_len &&
+ ib->selection_end >= text_position) {
+ guint sel_start = ibt->alloc.x, sel_width = ibt->alloc.width;
+ gint x_pos;
+ if (ib->selection_start > text_position) {
+ pango_layout_index_to_line_x(ibt->layout,
+ ib->selection_start - text_position,
+ FALSE, NULL, &x_pos);
+ sel_start += x_pos / PANGO_SCALE;
+ sel_width -= x_pos / PANGO_SCALE;
+ }
+ if (ib->selection_end < text_position + text_len) {
+ pango_layout_index_to_line_x(ibt->layout,
+ ib->selection_end - text_position,
+ FALSE, NULL, &x_pos);
+ sel_width -= ibt->alloc.width - x_pos / PANGO_SCALE;
+ }
+ /* todo: the following seems to render "inactive" selection,
+ but would be nice to render an active one */
+ gtk_style_context_add_class(styleCtx, "rubberband");
+ gtk_render_background(styleCtx, cr, sel_start, ibt->alloc.y,
+ sel_width, ibt->alloc.height);
+ gtk_style_context_remove_class(styleCtx, "rubberband");
+ }
+
+ gtk_render_layout(styleCtx, cr, ibt->alloc.x, ibt->alloc.y, ibt->layout);
+
+ if (ib->focused_object) {
+ if (IS_IB_LINK(ib->focused_object)) {
+ IBLink *ibl = IB_LINK(ib->focused_object);
+ if (ibl->start <= text_position + text_len &&
+ ibl->end > text_position) {
+ int start_index = 0, end_index = text_len;
+ if (ibl->start > text_position) {
+ start_index = ibl->start - text_position;
+ }
+ if (ibl->end < text_position + text_len) {
+ end_index = ibl->end - text_position;
+ }
+ int start_x = 0, end_x = 0;
+ pango_layout_index_to_line_x(ibt->layout, start_index,
+ 0, NULL, &start_x);
+ pango_layout_index_to_line_x(ibt->layout, end_index,
+ 0, NULL, &end_x);
+ gtk_render_focus(styleCtx, cr,
+ ibt->alloc.x + start_x / PANGO_SCALE,
+ ibt->alloc.y,
+ (end_x - start_x) / PANGO_SCALE,
+ ibt->alloc.height);
+ }
+ }
+ }
+ cairo_translate (cr, alloc.x, alloc.y);
+ text_position += text_len;
+ }
+ }
+ return FALSE;
+}
+
+static gboolean
+inline_box_focus (GtkWidget *widget,
+ GtkDirectionType direction)
+{
+ InlineBox *ib = INLINE_BOX(widget);
+ if (ib->children == NULL) {
+ return FALSE;
+ }
+ GList *ci;
+ guint text_position;
+ gboolean focus_next = FALSE;
+
+ if (ib->focused_object == NULL) {
+ focus_next = TRUE;
+ }
+
+ /* todo: allow moving focus inside a single word */
+ for (text_position = 0, ci = ib->children; ci; ci = ci->next) {
+ if (focus_next) {
+ if (GTK_IS_WIDGET(ci->data)) {
+ ib->focused_object = ci->data;
+ if (gtk_widget_child_focus(ci->data, direction)) {
+ gtk_widget_queue_draw(widget);
+ return TRUE;
+ }
+ } else if (ib->links != NULL && IS_IB_TEXT(ci->data)) {
+ GList *li;
+ for (li = ib->links; li; li = li->next) {
+ if (IB_LINK(li->data)->start <=
+ text_position +
+ strlen(pango_layout_get_text(IB_TEXT(ci->data)->layout)) &&
+ IB_LINK(li->data)->end > text_position) {
+ ib->focused_object = li->data;
+ gtk_widget_grab_focus(widget);
+ gtk_widget_queue_draw(widget);
+ return TRUE;
+ }
+ }
+ }
+ }
+ if (ci->data == ib->focused_object) {
+ focus_next = TRUE;
+ }
+
+ if (IS_IB_TEXT(ci->data)) {
+ text_position +=
+ strlen(pango_layout_get_text(IB_TEXT(ci->data)->layout));
+ if (IS_IB_LINK(ib->focused_object) &&
+ text_position >= IB_LINK(ib->focused_object)->end) {
+ focus_next = TRUE;
+ }
+ }
+ }
+ ib->focused_object = NULL;
+ gtk_widget_queue_draw(widget);
+ return FALSE;
+}
+
+
+static void
+inline_box_class_init (InlineBoxClass *klass)
+{
+ GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+ gobject_class->dispose = inline_box_dispose;
+ gobject_class->finalize = inline_box_finalize;
+
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass);
+ widget_class->get_request_mode = inline_box_get_request_mode;
+ widget_class->get_preferred_width = inline_box_get_preferred_width;
+ widget_class->get_preferred_height_for_width =
+ inline_box_get_preferred_height_for_width;
+ widget_class->size_allocate = inline_box_size_allocate;
+ widget_class->draw = inline_box_draw;
+ widget_class->focus = inline_box_focus;
+
+ GtkContainerClass *container_class = GTK_CONTAINER_CLASS(klass);
+ container_class->child_type = inline_box_child_type;
+ container_class->add = inline_box_add;
+ container_class->remove = inline_box_remove;
+ container_class->forall = inline_box_forall;
+}
+
+static void
+inline_box_init (InlineBox *ib)
+{
+ gtk_widget_set_has_window(GTK_WIDGET(ib), FALSE);
+ gtk_widget_set_can_focus(GTK_WIDGET(ib), TRUE);
+ INLINE_BOX(ib)->children = NULL;
+ INLINE_BOX(ib)->links = NULL;
+ INLINE_BOX(ib)->focused_object = NULL;
+}
+
+
+InlineBox *inline_box_new ()
+{
+ InlineBox *ib = INLINE_BOX(g_object_new(inline_box_get_type(), NULL));
+ ib->selection_start = 0;
+ ib->selection_end = 0;
+ ib->children = NULL;
+ ib->last_child = NULL;
+ ib->wrap = TRUE;
+ return ib;
+}
+
+static void inline_box_dispose (GObject *object)
+{
+ InlineBox *ib = INLINE_BOX(object);
+ if (ib->children != NULL) {
+ GList *il, *next;
+ for (il = ib->children; il; il = next) {
+ next = il->next;
+ if (IS_IB_TEXT(il->data) || IS_IB_BREAK(il->data)) {
+ g_object_unref(il->data);
+ ib->children = g_list_remove(ib->children, il->data);
+ }
+ }
+ }
+ if (ib->links != NULL) {
+ g_list_free_full(ib->links, g_object_unref);
+ ib->links = NULL;
+ }
+ G_OBJECT_CLASS (inline_box_parent_class)->dispose (object);
+}
+
+static void inline_box_finalize (GObject *object)
+{
+ InlineBox *ib = INLINE_BOX(object);
+ g_list_free(ib->children);
+ G_OBJECT_CLASS (inline_box_parent_class)->finalize (object);
+}
+
+static GtkSizeRequestMode inline_box_get_request_mode (GtkWidget *widget)
+{
+ return GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH;
+}
+
+static void
+inline_box_get_preferred_width(GtkWidget *widget, gint *minimal, gint *natural)
+{
+ GList *child;
+ gint child_min, child_nat, cur_natural;
+ *minimal = 0;
+ *natural = 0;
+ cur_natural = 0;
+ for(child = INLINE_BOX(widget)->children; child; child = child->next) {
+ if (GTK_IS_WIDGET(child->data)) {
+ gtk_widget_get_preferred_width(GTK_WIDGET(child->data),
+ &child_min, &child_nat);
+ if (*minimal < child_min) {
+ *minimal = child_min;
+ }
+ cur_natural += child_nat;
+ } else if (IS_IB_TEXT(child->data)) {
+ if (INLINE_BOX(widget)->wrap) {
+ if (IB_TEXT(child->data)->alloc.width > *minimal) {
+ *minimal = IB_TEXT(child->data)->alloc.width;
+ }
+ } else {
+ /* todo */
+ }
+ cur_natural += IB_TEXT(child->data)->alloc.width;
+ } else if (IS_IB_BREAK(child->data)) {
+ if (cur_natural > *natural) {
+ *natural = cur_natural;
+ }
+ cur_natural = 0;
+ }
+ }
+ if (cur_natural > *natural) {
+ *natural = cur_natural;
+ }
+}
+
+static void inline_box_get_preferred_height_for_width(GtkWidget *widget,
+ gint width,
+ gint *minimal,
+ gint *natural)
+{
+ GtkAllocation alloc, child_alloc;
+ GList *child;
+ gint child_min;
+ alloc.x = 0;
+ alloc.y = 0;
+ alloc.width = width;
+ alloc.height = 0;
+ *minimal = 0;
+ if (g_list_length(INLINE_BOX(widget)->children) > 0) {
+ /* todo: would be better to avoid reusing the same function, since
+ it actually allocates child window sizes. */
+ inline_box_size_allocate(widget, &alloc);
+ for(child = INLINE_BOX(widget)->children; child; child = child->next) {
+ child_min = 0;
+ if (GTK_IS_WIDGET(child->data)) {
+ gtk_widget_get_allocation(GTK_WIDGET(child->data), &child_alloc);
+ child_min = child_alloc.y + child_alloc.height;
+ } else if (IS_IB_TEXT(child->data)) {
+ child_min =
+ IB_TEXT(child->data)->alloc.y + IB_TEXT(child->data)->alloc.height;
+ }
+ if (*minimal < child_min) {
+ *minimal = child_min;
+ }
+ }
+ }
+ *natural = *minimal;
+}
+
+/* todo: this function is rather slow on larger books, in part because
+ of pango_layout_get_baseline, which is better to cache. Maybe
+ replace PangoLayout in IBText with its subtype, which would
+ include cached baseline. */
+int line_baseline(GList *iter, int full_width, gboolean wrap) {
+ int max_baseline = 0, line_width = 0, cur_baseline = 0;
+ for (; iter && (! IS_IB_BREAK(iter->data)); iter = iter->next) {
+ if (IS_IB_TEXT(iter->data)) {
+ cur_baseline = pango_layout_get_baseline(IB_TEXT(iter->data)->layout);
+ line_width += IB_TEXT(iter->data)->alloc.width;
+ } else if (GTK_IS_WIDGET(iter->data)) {
+ int w;
+ gtk_widget_get_preferred_width(iter->data, &w, NULL);
+ line_width += w;
+ }
+ if (wrap && (line_width > full_width)) {
+ break;
+ }
+ if (cur_baseline > max_baseline) {
+ max_baseline = cur_baseline;
+ }
+ }
+ max_baseline /= PANGO_SCALE;
+ return max_baseline;
+}
+
+static void
+inline_box_size_allocate (GtkWidget *widget, GtkAllocation *allocation)
+{
+ gtk_widget_set_allocation(widget, allocation);
+
+ unsigned border_width =
+ gtk_container_get_border_width(GTK_CONTAINER(widget));
+ int full_width = allocation->width - 2 * border_width;
+ int extra_width = full_width;
+
+ int x = allocation->x + border_width;
+ int y = allocation->y + border_width;
+ int line_height = 0, max_baseline;
+
+ GList *iter = INLINE_BOX(widget)->children;
+
+ max_baseline = line_baseline(iter, full_width, INLINE_BOX(widget)->wrap);
+
+ for(; iter; iter = iter->next) {
+ if (GTK_IS_WIDGET(iter->data)) {
+
+ if(!gtk_widget_get_visible(iter->data))
+ continue;
+
+ GtkAllocation child_allocation;
+ gtk_widget_get_preferred_width(iter->data, &child_allocation.width, NULL);
+ gtk_widget_get_preferred_height(iter->data, &child_allocation.height, NULL);
+
+ if (extra_width < child_allocation.width && extra_width < full_width) {
+ x = allocation->x + border_width;
+ y += line_height;
+ extra_width = full_width;
+ line_height = 0;
+ max_baseline = line_baseline(iter, full_width, INLINE_BOX(widget)->wrap);
+ }
+
+ child_allocation.x = x;
+ child_allocation.y = y;
+ gtk_widget_size_allocate(iter->data, &child_allocation);
+ extra_width -= child_allocation.width;
+ x += child_allocation.width;
+ line_height = line_height > child_allocation.height
+ ? line_height
+ : child_allocation.height;
+ } else if (IS_IB_TEXT(iter->data)) {
+ IBText *ibt = IB_TEXT(iter->data);
+ if (INLINE_BOX(widget)->wrap && extra_width < ibt->alloc.width &&
+ extra_width < full_width) {
+ x = allocation->x + border_width;
+ y += line_height;
+ extra_width = full_width;
+ line_height = 0;
+ max_baseline = line_baseline(iter, full_width, INLINE_BOX(widget)->wrap);
+ }
+ int y_offset = max_baseline - pango_layout_get_baseline(ibt->layout) / PANGO_SCALE;
+ ibt->alloc.x = x;
+ ibt->alloc.y = y + y_offset;
+
+ if ((guint)x == allocation->x + border_width &&
+ INLINE_BOX(widget)->wrap &&
+ strcmp(pango_layout_get_text(IB_TEXT(iter->data)->layout), " ") == 0) {
+ /* A space in the beginning of a line, not in <pre> */
+ } else {
+ extra_width -= ibt->alloc.width;
+ x += ibt->alloc.width;
+ line_height = line_height > (ibt->alloc.height + y_offset)
+ ? line_height
+ : (ibt->alloc.height + y_offset);
+ }
+ } else if (IS_IB_BREAK(iter->data)) {
+ x = allocation->x + border_width;
+ y += line_height;
+ extra_width = full_width;
+ max_baseline = line_baseline(iter->next, full_width, INLINE_BOX(widget)->wrap);
+ }
+ }
+}
+
+static GType
+inline_box_child_type(GtkContainer *container)
+{
+ return GTK_TYPE_WIDGET;
+}
+
+void inline_box_add_text(InlineBox *container, IBText *text)
+{
+ container->last_child = g_list_append(container->last_child, text);
+ if (container->children == NULL) {
+ container->children = container->last_child;
+ }
+ if (container->last_child->next != NULL) {
+ container->last_child = container->last_child->next;
+ }
+ gtk_widget_queue_resize(GTK_WIDGET(container));
+}
+
+void inline_box_break(InlineBox *container)
+{
+ container->last_child = g_list_append(container->last_child, ib_break_new());
+ if (container->children == NULL) {
+ container->children = container->last_child;
+ }
+ if (container->last_child->next != NULL) {
+ container->last_child = container->last_child->next;
+ }
+ gtk_widget_queue_resize(GTK_WIDGET(container));
+}
+
+
+static void
+inline_box_add(GtkContainer *container, GtkWidget *widget)
+{
+ InlineBox *ib = INLINE_BOX(container);
+ ib->last_child = g_list_append(ib->last_child, widget);
+ if (ib->children == NULL) {
+ ib->children = ib->last_child;
+ }
+ if (ib->last_child->next != NULL) {
+ ib->last_child = ib->last_child->next;
+ }
+ gtk_widget_set_parent(widget, GTK_WIDGET(container));
+ if(gtk_widget_get_visible(widget))
+ gtk_widget_queue_resize(GTK_WIDGET(container));
+}
+
+static void
+inline_box_remove(GtkContainer *container, GtkWidget *widget)
+{
+ InlineBox *ib = INLINE_BOX (container);
+ gtk_widget_unparent (widget);
+ ib->children = g_list_remove (ib->children, widget);
+}
+
+static void
+inline_box_forall (GtkContainer *container, gboolean include_internals,
+ GtkCallback callback, gpointer callback_data)
+{
+ InlineBox *ib = INLINE_BOX (container);
+ GList *child, *next;
+ child = ib->children;
+ while (child) {
+ /* Current child can be removed and freed, so better remember the
+ next one before running the callback. */
+ next = child->next;
+ if (child && child->data && GTK_IS_WIDGET(child->data)) {
+ (* callback) (GTK_WIDGET(child->data), callback_data);
+ }
+ child = next;
+ }
+}
diff --git a/src/inlinebox.h b/src/inlinebox.h
new file mode 100644
index 0000000..03e5ec8
--- /dev/null
+++ b/src/inlinebox.h
@@ -0,0 +1,119 @@
+/* WWWLite, a lightweight web browser.
+ Copyright (C) 2019 defanor
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+*/
+
+#ifndef INLINE_BOX_H
+#define INLINE_BOX_H
+
+#include <glib-object.h>
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+
+/* inline box text */
+
+/* Using just a GObject for it (and not a GtkWidget), since it's a few
+ times slower with GtkWidget. */
+
+#define IB_TEXT_TYPE (ib_text_get_type())
+G_DECLARE_FINAL_TYPE (IBText, ib_text, IB, TEXT, GObject);
+
+struct _IBText {
+ GObject parent_instance;
+ PangoLayout *layout;
+ GtkAllocation alloc;
+};
+
+#define IS_IB_TEXT(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), IB_TEXT_TYPE))
+
+IBText* ib_text_new (PangoLayout *layout);
+gboolean ib_text_at_point(IBText *ibt, gint x, gint y, gint *position);
+
+
+/* line break */
+
+#define IB_BREAK_TYPE (ib_break_get_type())
+G_DECLARE_FINAL_TYPE (IBBreak, ib_break, IB, BREAK, GObject);
+
+struct _IBBreak
+{
+ GObject parent_instance;
+};
+
+#define IS_IB_BREAK(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), IB_BREAK_TYPE))
+IBBreak *ib_break_new ();
+
+
+
+/* link */
+
+#define IB_LINK_TYPE (ib_link_get_type())
+G_DECLARE_FINAL_TYPE (IBLink, ib_link, IB, LINK, GObject);
+
+struct _IBLink
+{
+ GObject parent_instance;
+ guint start;
+ guint end;
+ GList *objects; /* todo */
+ gchar *url;
+};
+
+#define IS_IB_LINK(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), IB_LINK_TYPE))
+IBLink *ib_link_new (const gchar *url);
+
+
+/* inline box */
+
+#define INLINE_BOX_TYPE (inline_box_get_type())
+#define INLINE_BOX(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), INLINE_BOX_TYPE, InlineBox))
+#define INLINE_BOX_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), INLINE_BOX_TYPE, InlineBoxClass))
+#define IS_INLINE_BOX(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), INLINE_BOX_TYPE))
+#define IS_INLINE_BOX_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), INLINE_BOX_TYPE))
+
+typedef struct _InlineBox InlineBox;
+typedef struct _InlineBoxClass InlineBoxClass;
+
+struct _InlineBox
+{
+ GtkContainer parent_instance;
+ GList *children;
+ GList *last_child;
+ /* It would be cleaner to store links as children, but that would
+ require additional functions to manage children. Keeping a
+ separate list for now; probably it's not worth the complication,
+ and it's only needed to manage/draw focus. */
+ GList *links;
+ GObject *focused_object;
+ guint selection_start;
+ guint selection_end;
+ gboolean wrap;
+};
+
+struct _InlineBoxClass
+{
+ GtkContainerClass parent_class;
+};
+
+GType inline_box_get_type(void) G_GNUC_CONST;
+InlineBox *inline_box_new(void);
+void inline_box_add_text(InlineBox *container, IBText *text);
+void inline_box_break(InlineBox *container);
+
+G_END_DECLS
+
+#endif
diff --git a/src/main.c b/src/main.c
new file mode 100644
index 0000000..65adadf
--- /dev/null
+++ b/src/main.c
@@ -0,0 +1,134 @@
+/* WWWLite, a lightweight web browser.
+ Copyright (C) 2019 defanor
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+*/
+
+#include <glib.h>
+#include <gtk/gtk.h>
+#include "browserbox.h"
+
+gchar **start_uri = NULL;
+
+static GOptionEntry entries[] =
+{
+ { G_OPTION_REMAINING, 0, G_OPTION_FLAG_NONE,
+ G_OPTION_ARG_STRING_ARRAY, &start_uri, "URI", NULL },
+ { NULL }
+};
+
+static gboolean
+key_press_event_cb (GtkWidget *widget, GdkEventKey *ev, GtkStack *tabs)
+{
+ if (ev->state & GDK_CONTROL_MASK) {
+ if (ev->keyval == GDK_KEY_t) {
+ GtkWidget *browser_box = GTK_WIDGET(browser_box_new(NULL));
+ BROWSER_BOX(browser_box)->tabs = tabs;
+ gtk_stack_add_titled(tabs, browser_box, "New tab", "New tab");
+ gtk_widget_show_all(browser_box);
+ gtk_stack_set_visible_child(tabs, browser_box);
+ return TRUE;
+ } else if (ev->keyval == GDK_KEY_w) {
+ GtkWidget *current_tab = gtk_stack_get_visible_child(tabs);
+ if (current_tab) {
+ gtk_widget_destroy(current_tab);
+ }
+ return TRUE;
+ }
+ }
+ GtkWidget *current_tab = gtk_stack_get_visible_child(tabs);
+ if (current_tab != NULL) {
+ BrowserBox *bb = BROWSER_BOX(current_tab);
+ if (ev->keyval == GDK_KEY_Back || ev->keyval == GDK_KEY_BackSpace) {
+ return history_back(bb);
+ } else if (ev->keyval == GDK_KEY_Forward) {
+ return history_forward(bb);
+ }
+ }
+ return FALSE;
+}
+
+static gboolean
+button_press_event_cb (GtkWidget *widget, GdkEventButton *ev, GtkStack *tabs)
+{
+ GtkWidget *current_tab = gtk_stack_get_visible_child(tabs);
+ if (current_tab != NULL) {
+ BrowserBox *bb = BROWSER_BOX(current_tab);
+ if (ev->button == 8) {
+ return history_back(bb);
+ } else if (ev->button == 9) {
+ return history_forward(bb);
+ }
+ }
+ return FALSE;
+}
+
+static void activate (GtkApplication *app, gpointer user_data)
+{
+ GtkWidget *window;
+
+ window = gtk_application_window_new (app);
+ gtk_window_resize(GTK_WINDOW(window), 800, 800);
+ gtk_window_set_title (GTK_WINDOW (window), "WWWLite");
+
+ GtkWidget *evbox = gtk_event_box_new();
+ gtk_container_add (GTK_CONTAINER (window), evbox);
+
+ GtkWidget *box = block_box_new(0);
+ gtk_container_add (GTK_CONTAINER (evbox), box);
+
+ GtkWidget *switcher = gtk_stack_switcher_new();
+ gtk_container_add (GTK_CONTAINER (box), switcher);
+
+ GtkWidget *stack = gtk_stack_new();
+ gtk_stack_switcher_set_stack(GTK_STACK_SWITCHER(switcher), GTK_STACK(stack));
+ gtk_container_add (GTK_CONTAINER (box), stack);
+ gtk_box_set_child_packing(GTK_BOX(box), stack, TRUE, TRUE, 0, GTK_PACK_START);
+
+ GtkWidget *browser_box = GTK_WIDGET(browser_box_new(start_uri != NULL ? start_uri[0] : NULL));
+ gtk_stack_add_titled(GTK_STACK(stack), browser_box, "Tab 1", "Tab 1");
+ BROWSER_BOX(browser_box)->tabs = GTK_STACK(stack);
+
+ g_signal_connect (evbox, "key-press-event",
+ G_CALLBACK (key_press_event_cb), stack);
+ g_signal_connect (evbox, "button-release-event",
+ G_CALLBACK (button_press_event_cb), stack);
+
+ gtk_widget_show_all (window);
+
+ word_cache = g_hash_table_new((GHashFunc)wck_hash, (GEqualFunc)wck_equal);
+ return;
+}
+
+int
+main (int argc, char **argv)
+{
+ GOptionContext *context = g_option_context_new ("[URI]");
+ g_option_context_add_main_entries (context, entries, NULL);
+ g_option_context_add_group (context, gtk_get_option_group(TRUE));
+ GError *error = NULL;
+ GtkApplication *app;
+ int status;
+ if (! g_option_context_parse (context, &argc, &argv, &error)) {
+ g_print("Failed to parse arguments: %s\n", error->message);
+ exit(1);
+ }
+
+ app = gtk_application_new (NULL, G_APPLICATION_FLAGS_NONE);
+ g_signal_connect (app, "activate", G_CALLBACK (activate), NULL);
+ status = g_application_run (G_APPLICATION (app), argc, argv);
+ g_object_unref (app);
+
+ return status;
+}
diff --git a/src/tablebox.c b/src/tablebox.c
new file mode 100644
index 0000000..27e1f3d
--- /dev/null
+++ b/src/tablebox.c
@@ -0,0 +1,407 @@
+/* WWWLite, a lightweight web browser.
+ Copyright (C) 2019 defanor
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+*/
+
+#include <gtk/gtk.h>
+#include <math.h>
+#include "tablebox.h"
+
+G_DEFINE_TYPE (TableCell, table_cell, BLOCK_BOX_TYPE);
+G_DEFINE_TYPE (TableBox, table_box, GTK_TYPE_CONTAINER);
+
+
+/* cell */
+
+static void
+table_cell_class_init (TableCellClass *klass)
+{
+ return;
+}
+
+static void
+table_cell_init (TableCell *tc)
+{
+ tc->colspan = 1;
+ tc->rowspan = 1;
+ return;
+}
+
+GtkWidget *
+table_cell_new ()
+{
+ TableCell *tc = TABLE_CELL(g_object_new(table_cell_get_type(),
+ "orientation", GTK_ORIENTATION_VERTICAL,
+ "spacing", 10,
+ NULL));
+ return GTK_WIDGET(tc);
+}
+
+
+/* table */
+
+static GType table_box_child_type (GtkContainer *container);
+static void table_box_add (GtkContainer *container, GtkWidget *widget);
+static void table_box_size_allocate (GtkWidget *widget,
+ GtkAllocation *allocation);
+static void table_box_forall (GtkContainer *container,
+ gboolean include_internals,
+ GtkCallback callback, gpointer callback_data);
+static void table_box_remove (GtkContainer *container, GtkWidget *widget);
+static GtkSizeRequestMode table_box_get_request_mode (GtkWidget *widget);
+static void table_box_get_preferred_width(GtkWidget *widget,
+ gint *minimal, gint *natural);
+static void table_box_get_preferred_height_for_width(GtkWidget *widget,
+ gint width,
+ gint *minimal,
+ gint *natural);
+static void table_box_column_widths (TableBox *tb, GList **min_widths, GList **nat_widths);
+static void table_box_finalize (GObject *object);
+
+static guint padding = 10;
+
+static void
+table_box_class_init (TableBoxClass *klass)
+{
+ GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+ gobject_class->finalize = table_box_finalize;
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass);
+ widget_class->size_allocate = table_box_size_allocate;
+ widget_class->get_request_mode = table_box_get_request_mode;
+ widget_class->get_preferred_width = table_box_get_preferred_width;
+ widget_class->get_preferred_height_for_width =
+ table_box_get_preferred_height_for_width;
+ GtkContainerClass *container_class = GTK_CONTAINER_CLASS(klass);
+ container_class->child_type = table_box_child_type;
+ container_class->add = table_box_add;
+ container_class->forall = table_box_forall;
+ container_class->remove = table_box_remove;
+ return;
+}
+
+static void
+table_box_init (TableBox *tb)
+{
+ gtk_widget_set_has_window(GTK_WIDGET(tb), FALSE);
+ tb->rows = NULL;
+}
+
+static void table_box_finalize (GObject *object)
+{
+ TableBox *tb = TABLE_BOX(object);
+ g_list_free_full(tb->rows, (GDestroyNotify)g_list_free);
+ G_OBJECT_CLASS (table_box_parent_class)->finalize (object);
+}
+
+GtkWidget *
+table_box_new ()
+{
+ TableBox *tb = TABLE_BOX(g_object_new(table_box_get_type(),
+ NULL));
+ return GTK_WIDGET(tb);
+}
+
+void
+table_box_add_row (TableBox *tb)
+{
+ tb->rows = g_list_append(tb->rows, NULL);
+ return;
+}
+
+static GtkSizeRequestMode
+table_box_get_request_mode (GtkWidget *widget)
+{
+ return GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH;
+}
+
+static void
+table_box_get_preferred_width(GtkWidget *widget,
+ gint *minimal, gint *natural)
+{
+ TableBox *tb = TABLE_BOX(widget);
+ GList *minimal_widths, *natural_widths, *iter;
+ table_box_column_widths(tb, &minimal_widths, &natural_widths);
+
+ if (minimal != NULL) {
+ *minimal = -padding;
+ for (iter = minimal_widths; iter; iter = iter->next) {
+ *minimal += GPOINTER_TO_INT(iter->data) + padding;
+ }
+ }
+ if (natural != NULL) {
+ *natural = -padding;
+ for (iter = natural_widths; iter; iter = iter->next) {
+ *natural += GPOINTER_TO_INT(iter->data) + padding;
+ }
+ }
+ g_list_free(minimal_widths);
+ g_list_free(natural_widths);
+}
+
+static void
+table_box_get_preferred_height_for_width(GtkWidget *widget,
+ gint width,
+ gint *minimal,
+ gint *natural)
+{
+ GtkAllocation alloc, child_alloc;
+ alloc.x = 0;
+ alloc.y = 0;
+ alloc.width = width;
+ alloc.height = 0;
+ table_box_size_allocate(widget, &alloc);
+
+ TableBox *tb = TABLE_BOX(widget);
+ GList *row, *cell;
+ *minimal = 0;
+ for (row = tb->rows; row; row = row->next) {
+ for (cell = row->data; cell; cell = cell->next) {
+ gtk_widget_get_allocation(cell->data, &child_alloc);
+ if (*minimal < child_alloc.y + child_alloc.height) {
+ *minimal = child_alloc.y + child_alloc.height;
+ }
+ }
+ }
+ *natural = *minimal;
+}
+
+static GType
+table_box_child_type (GtkContainer *container)
+{
+ return TABLE_CELL_TYPE;
+}
+
+static void
+table_box_add (GtkContainer *container, GtkWidget *widget)
+{
+ TableBox *tb = TABLE_BOX(container);
+ GList *row = g_list_last(tb->rows);
+ row->data = g_list_append(row->data, widget);
+ gtk_widget_set_parent(widget, GTK_WIDGET(container));
+ if (gtk_widget_get_visible(widget))
+ gtk_widget_queue_resize(GTK_WIDGET(container));
+}
+
+static void
+table_box_forall (GtkContainer *container, gboolean include_internals,
+ GtkCallback callback, gpointer callback_data)
+{
+ GList *row, *cell, *next_row, *next_cell;
+ row = TABLE_BOX(container)->rows;
+ while (row) {
+ next_row = row->next;
+ cell = row->data;
+ while (cell) {
+ next_cell = cell->next;
+ (* callback) (GTK_WIDGET(cell->data), callback_data);
+ cell = next_cell;
+ }
+ row = next_row;
+ }
+}
+
+static void
+table_box_remove (GtkContainer *container, GtkWidget *widget)
+{
+ TableBox *tb = TABLE_BOX (container);
+ gtk_widget_unparent (widget);
+ GList *row, *cell;
+ for (row = tb->rows; row; row = row->next) {
+ for (cell = row->data; cell; cell = cell->next) {
+ if (cell->data == widget) {
+ row->data = g_list_delete_link(row->data, cell);
+ return;
+ }
+ }
+ }
+}
+
+static void
+table_box_column_widths (TableBox *tb, GList **min_widths, GList **nat_widths)
+{
+ /* todo: would be nice to ensure that all the columns end up being
+ of approximately the same height */
+ GList *row, *cell;
+ GList *minimal_widths = NULL;
+ GList *natural_widths = NULL;
+ GList *descending_cells = NULL;
+ gint cell_x, cell_y, cell_next_x;
+ cell_y = 0;
+ for (row = tb->rows; row; row = row->next) {
+ cell_x = 0;
+ for (cell = row->data; cell; cell = cell->next) {
+ /* Skip the cells spanning from above */
+ while (g_list_nth_data(descending_cells, cell_x)) {
+ cell_x++;
+ }
+
+ TableCell *tc = cell->data;
+ gint min, nat;
+ gtk_widget_get_preferred_width(GTK_WIDGET(tc), &min, &nat);
+ min /= tc->colspan;
+ nat /= tc->colspan;
+ for (cell_next_x = cell_x; cell_next_x < cell_x + tc->colspan; cell_next_x++) {
+ while (g_list_nth(minimal_widths, cell_next_x) == NULL) {
+ minimal_widths = g_list_append(minimal_widths, GINT_TO_POINTER(0));
+ }
+ while (g_list_nth(natural_widths, cell_next_x) == NULL) {
+ natural_widths = g_list_append(natural_widths, GINT_TO_POINTER(0));
+ }
+ while (g_list_nth(descending_cells, cell_next_x) == NULL) {
+ descending_cells = g_list_append(descending_cells, GINT_TO_POINTER(0));
+ }
+ GList *col_min_width = g_list_nth(minimal_widths, cell_next_x);
+ if (GPOINTER_TO_INT(col_min_width->data) < min) {
+ col_min_width->data = GINT_TO_POINTER(min);
+ }
+ GList *col_nat_width = g_list_nth(natural_widths, cell_next_x);
+ if (GPOINTER_TO_INT(col_nat_width->data) < nat) {
+ col_nat_width->data = GINT_TO_POINTER(nat);
+ }
+ /* Update descending cells */
+ GList *descending_cell = g_list_nth(descending_cells, cell_next_x);
+ descending_cell->data = GINT_TO_POINTER(tc->rowspan);
+ }
+ cell_x += tc->colspan;
+ }
+ GList *descending_cell;
+ for (descending_cell = descending_cells; descending_cell;
+ descending_cell = descending_cell->next) {
+ if (GPOINTER_TO_INT(descending_cell->data) > 0) {
+ descending_cell->data--;
+ }
+ }
+ cell_y++;
+ }
+ *min_widths = minimal_widths;
+ *nat_widths = natural_widths;
+ g_list_free(descending_cells);
+}
+
+static void
+table_box_size_allocate (GtkWidget *widget, GtkAllocation *allocation)
+{
+ gtk_widget_set_allocation(widget, allocation);
+ guint border_width = gtk_container_get_border_width(GTK_CONTAINER(widget));
+ gint full_width = allocation->width - 2 * border_width;
+ TableBox *tb = TABLE_BOX(widget);
+ GList *row, *cell;
+ gint x;
+ gint y = allocation->y + border_width;
+ gint row_height;
+
+ GList *minimal_widths = NULL;
+ GList *natural_widths = NULL;
+ gint *descending_cells;
+ gint *pending_heights;
+ gint *actual_widths;
+ gint cell_x, cell_y, cell_next_x;
+
+ int natural_width;
+ table_box_get_preferred_width(widget, NULL, &natural_width);
+ gdouble shrinking = (gdouble)natural_width / (gdouble)full_width;
+
+ table_box_column_widths(tb, &minimal_widths, &natural_widths);
+ gint col_cnt = g_list_length(minimal_widths);
+
+ descending_cells = g_malloc0(sizeof(gint) * col_cnt);
+ pending_heights = g_malloc0(sizeof(gint) * col_cnt);
+ actual_widths = g_malloc0(sizeof(gint) * col_cnt);
+
+ gint extra_width = full_width + padding;
+
+ /* Assign minimal widths to columns */
+ for (cell_x = 0, cell = minimal_widths; cell; cell_x++, cell = cell->next) {
+ gint minimal_width = GPOINTER_TO_INT(cell->data);
+ actual_widths[cell_x] = minimal_width;
+ extra_width -= actual_widths[cell_x] + padding;
+ }
+ /* Distribute remaining width */
+ for (cell_x = 0, cell = natural_widths; cell && extra_width > 0; cell_x++, cell = cell->next) {
+ gint natural_width = GPOINTER_TO_INT(cell->data);
+ if (shrinking <= 1.0) {
+ extra_width -= natural_width - actual_widths[cell_x];
+ actual_widths[cell_x] = natural_width;
+ } else if (natural_width / shrinking > actual_widths[cell_x]) {
+ if (extra_width > natural_width / shrinking - actual_widths[cell_x]) {
+ extra_width -= natural_width / shrinking - actual_widths[cell_x];
+ actual_widths[cell_x] = natural_width / shrinking;
+ } else {
+ actual_widths[cell_x] += extra_width;
+ extra_width = 0;
+ }
+ }
+ }
+
+ cell_y = 0;
+ for (row = tb->rows; row; row = row->next) {
+ cell_x = 0;
+ x = allocation->x + border_width;
+ row_height = 0;
+ for (cell = row->data; cell; cell = cell->next) {
+ /* Skip the cells spanning from above */
+ while (cell_x < col_cnt && descending_cells[cell_x] > 0) {
+ x += actual_widths[cell_x] + padding;
+ cell_x++;
+ }
+ TableCell *tc = cell->data;
+ GtkAllocation child_alloc;
+ child_alloc.width = -padding;
+ for (cell_next_x = cell_x; cell_next_x < cell_x + tc->colspan; cell_next_x++) {
+ child_alloc.width += actual_widths[cell_next_x] + padding;
+ }
+ gtk_widget_get_preferred_height_for_width(cell->data, child_alloc.width,
+ &child_alloc.height, NULL);
+ child_alloc.x = x;
+ child_alloc.y = y;
+ gtk_widget_size_allocate(cell->data, &child_alloc);
+ x += child_alloc.width + padding;
+
+ for (cell_next_x = cell_x; cell_next_x < cell_x + tc->colspan; cell_next_x++) {
+ /* Update descending cells and pending heights */
+ descending_cells[cell_next_x] = tc->rowspan;
+ pending_heights[cell_next_x] = child_alloc.height;
+ }
+ cell_x += tc->colspan;
+ }
+ /* Update descending cells and pending heights, and row_height
+ based on those. */
+ for (cell_x = 0; cell_x < col_cnt; cell_x++) {
+ if (descending_cells[cell_x] > 0) {
+ descending_cells[cell_x]--;
+ if (descending_cells[cell_x] == 0) {
+ if (pending_heights[cell_x] > row_height) {
+ row_height = pending_heights[cell_x];
+ }
+ }
+ }
+ }
+ for (cell_x = 0; cell_x < (gint)g_list_length(minimal_widths); cell_x++) {
+ if (pending_heights[cell_x] > row_height) {
+ pending_heights[cell_x] -= row_height;
+ } else {
+ pending_heights[cell_x] = 0;
+ }
+ }
+ y += row_height;
+ cell_y++;
+ }
+
+ g_list_free(minimal_widths);
+ g_list_free(natural_widths);
+ free(actual_widths);
+ free(descending_cells);
+ free(pending_heights);
+}
diff --git a/src/tablebox.h b/src/tablebox.h
new file mode 100644
index 0000000..3848e30
--- /dev/null
+++ b/src/tablebox.h
@@ -0,0 +1,84 @@
+/* WWWLite, a lightweight web browser.
+ Copyright (C) 2019 defanor
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+*/
+
+#ifndef TABLE_BOX_H
+#define TABLE_BOX_H
+
+#include <gtk/gtk.h>
+#include "blockbox.h"
+
+G_BEGIN_DECLS
+
+/* cell */
+
+#define TABLE_CELL_TYPE (table_cell_get_type())
+#define TABLE_CELL(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), TABLE_CELL_TYPE, TableCell))
+#define TABLE_CELL_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), TABLE_CELL_TYPE, TableCellClass))
+#define IS_TABLE_CELL(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), TABLE_CELL_TYPE))
+#define IS_TABLE_CELL_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), TABLE_CELL_TYPE))
+
+typedef struct _TableCell TableCell;
+typedef struct _TableCellClass TableCellClass;
+
+struct _TableCell
+{
+ BlockBox parent_instance;
+ gint rowspan;
+ gint colspan;
+};
+
+struct _TableCellClass
+{
+ BlockBoxClass parent_class;
+};
+
+GType table_cell_get_type(void) G_GNUC_CONST;
+GtkWidget *table_cell_new();
+
+
+/* table */
+
+#define TABLE_BOX_TYPE (table_box_get_type())
+#define TABLE_BOX(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), TABLE_BOX_TYPE, TableBox))
+#define TABLE_BOX_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), TABLE_BOX_TYPE, TableBoxClass))
+#define IS_TABLE_BOX(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), TABLE_BOX_TYPE))
+#define IS_TABLE_BOX_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), TABLE_BOX_TYPE))
+
+typedef struct _TableBox TableBox;
+typedef struct _TableBoxClass TableBoxClass;
+
+struct _TableBox
+{
+ GtkContainer parent_instance;
+ GList *rows;
+};
+
+struct _TableBoxClass
+{
+ GtkContainerClass parent_class;
+};
+
+GType table_box_get_type(void) G_GNUC_CONST;
+GtkWidget *table_box_new();
+void table_box_add_row(TableBox *tb);
+/* void table_box_get_dimensions (TableBox *tb, guint *cols, guint *rows); */
+/* void table_box_get_column_widths (TableBox *tb, gint *minimal, gint *natural); */
+
+
+G_END_DECLS
+
+#endif